openlearn-next 0.7.10 → 0.7.13
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 +9 -2
- package/dist/assets/{ActivityWorkspaceWidget-BU88GGUG.js → ActivityWorkspaceWidget-9DeXkxGk.js} +1 -1
- package/dist/assets/AdminDirectoryView-5gupeGAR.js +5 -0
- package/dist/assets/{AssessmentCenter-BoSWbJMt.js → AssessmentCenter-CPaqy0Xa.js} +1 -1
- package/dist/assets/{AssignmentFilePreview-D_POvLmv.js → AssignmentFilePreview-7fHtUuUL.js} +2 -2
- package/dist/assets/{BatchPickerModal-DiHd_6Tg.js → BatchPickerModal-0yIe1DXi.js} +1 -1
- package/dist/assets/{ClassesView-CWKqzWH0.js → ClassesView-CGpoW0IC.js} +5 -5
- package/dist/assets/{ClassroomCountdownWidget-B5-KUVU8.js → ClassroomCountdownWidget-B_5--dn_.js} +1 -1
- package/dist/assets/{CloudDriveModal-CjcvdDiw.js → CloudDriveModal-4Ts5Duh5.js} +1 -1
- package/dist/assets/{ComputerLabView-CNTcmWAh.js → ComputerLabView-DAEaKTBj.js} +1 -1
- package/dist/assets/{CourseManagement-ha_iSnLU.js → CourseManagement-CxIBH7lA.js} +1 -1
- package/dist/assets/{CourseWizardModal-oVT9mYSG.js → CourseWizardModal-R0xpa_Aw.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-BuKp0mmW.js → CoursewareHubPanel-CpHptmSa.js} +1 -1
- package/dist/assets/{Dashboard-Vb9coh9_.js → Dashboard-DeBfP_Nc.js} +2 -2
- package/dist/assets/{ExportWeightModal-BEesz6rd.js → ExportWeightModal-DlnCOBlL.js} +1 -1
- package/dist/assets/{HelpTour-D3QcGDUz.js → HelpTour-Cztpqw_J.js} +1 -1
- package/dist/assets/{HelpView--Um9DFYx.js → HelpView-LPLAY9bq.js} +2 -2
- package/dist/assets/{ImportLessonsModal-DXGopj4j.js → ImportLessonsModal-BopEHmU_.js} +1 -1
- package/dist/assets/ImportModal-CYU3aMYY.js +3 -0
- package/dist/assets/{InteractiveCoursewareViewer-DzgF9_LU.js → InteractiveCoursewareViewer-CGc3P4HF.js} +1 -1
- package/dist/assets/InteractiveWhiteboard-DiyHrIGn.js +19 -0
- package/dist/assets/{LazyCourseware-6yb_QiYS.js → LazyCourseware-Bj1jm0nC.js} +2 -2
- package/dist/assets/LazyWhiteboard-NPD1EoyO.js +2 -0
- package/dist/assets/LessonEditorView-DqreBtFx.js +1 -0
- package/dist/assets/LiveClassroomView-eTc6Fb9g.js +64 -0
- package/dist/assets/{NotificationDetailModal-mUeIQP4R.js → NotificationDetailModal-B7G7ra9a.js} +1 -1
- package/dist/assets/{PluginView-CT3-zS2n.js → PluginView-DP6s-L70.js} +1 -1
- package/dist/assets/{ProcessLogsModal-DmsOrZnV.js → ProcessLogsModal-Dq5C4s7j.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-ViKxNX5Q.js → QuizGeneratorModal-CoX9f8Wp.js} +1 -1
- package/dist/assets/RevealPresentationWrapper-wwlcfNWB.js +48 -0
- package/dist/assets/{StudentAssignmentEvalPanel-BRxHMABY.js → StudentAssignmentEvalPanel-DHb-asHQ.js} +1 -1
- package/dist/assets/{StudentAssignmentView-g-ewJIS3.js → StudentAssignmentView-AJpjQK3f.js} +1 -1
- package/dist/assets/{StudentLessonView-Dbvr3Xlo.js → StudentLessonView-Djd1T1QF.js} +1 -1
- package/dist/assets/{StudentView-20vfEk4v.js → StudentView-D41z7GY5.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-CDKeaKf8.js → SystemResourceLibraryModal-CxmQh1HA.js} +1 -1
- package/dist/assets/TeacherView-DqSWaNuI.js +2 -0
- package/dist/assets/TimetableView-DrFs_xgc.js +6 -0
- package/dist/assets/{UTIF-BtQhea94.js → UTIF-u3Ct70K4.js} +1 -1
- package/dist/assets/index-7CK1B0tE.js +131 -0
- package/dist/assets/index-BaGZmpr6.css +1 -0
- package/dist/assets/{index.es-CijVATXs.js → index.es-CcQ2kcDj.js} +1 -1
- package/dist/assets/pluginParsers-BN3lQ7PJ.js +1 -0
- package/dist/assets/{student-workspace-DstiW0pM.js → student-workspace-DkTkcAGK.js} +1 -1
- package/dist/assets/{vendor-icons-DLOPlM4i.js → vendor-icons-CqLdl4B_.js} +1 -1
- package/dist/assets/{vendor-pdf-DVWGghIL.js → vendor-pdf-BMEc_Clb.js} +3 -3
- package/dist/assets/{vendor-pptx-BqU7v5zS.js → vendor-pptx-C0qaOH5T.js} +178 -178
- package/dist/index.html +3 -3
- package/dist/server.cjs +1666 -1846
- package/migrations/045_ai_providers_owner_and_scope.sql +20 -0
- package/migrations/README.md +1 -0
- package/migrations/schema-golden.json +13 -3
- package/package.json +5 -2
- package/dist/assets/AdminDirectoryView-Cdsk9Mdr.js +0 -5
- package/dist/assets/ImportModal-h5ShIMzO.js +0 -2
- package/dist/assets/InteractiveWhiteboard-DJqgP_tU.js +0 -65
- package/dist/assets/LazyWhiteboard-CsF9GB6J.js +0 -2
- package/dist/assets/LessonEditorView-CZTZ7Q0B.js +0 -1
- package/dist/assets/LiveClassroomView-D2ugupT9.js +0 -61
- package/dist/assets/TeacherView-KIKuhXGF.js +0 -2
- package/dist/assets/TimetableView-7V2AwWml.js +0 -6
- package/dist/assets/index-C4cHQZyx.css +0 -1
- package/dist/assets/index-XduPa2-x.js +0 -131
- package/dist/assets/pluginParsers-BO6U1E-h.js +0 -1
- package/dist/assets/vendor-reveal-DcQQzuI9.css +0 -1
package/dist/server.cjs
CHANGED
|
@@ -3304,6 +3304,95 @@ var init_ai_event_bus = __esm({
|
|
|
3304
3304
|
}
|
|
3305
3305
|
});
|
|
3306
3306
|
|
|
3307
|
+
// packages/core/di/ai-provider-resolver.ts
|
|
3308
|
+
function hasOwnerColumns(db2) {
|
|
3309
|
+
try {
|
|
3310
|
+
const cols = db2.prepare("PRAGMA table_info(ai_providers)").all();
|
|
3311
|
+
return cols.some((c) => c.name === "owner_id");
|
|
3312
|
+
} catch {
|
|
3313
|
+
return false;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
function resolveAccessibleAiProvider(db2, options = {}) {
|
|
3317
|
+
const { providerId, userId, lang = "zh" } = options;
|
|
3318
|
+
const supportsOwner = hasOwnerColumns(db2);
|
|
3319
|
+
let provider;
|
|
3320
|
+
if (providerId) {
|
|
3321
|
+
const row = supportsOwner ? db2.prepare(
|
|
3322
|
+
"SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at FROM ai_providers WHERE id = ?"
|
|
3323
|
+
).get(providerId) : db2.prepare("SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE id = ?").get(providerId);
|
|
3324
|
+
if (!row) {
|
|
3325
|
+
throw new AiProviderError(
|
|
3326
|
+
lang === "zh" ? "\u672A\u627E\u5230\u6307\u5B9A\u7684 AI \u63D0\u4F9B\u5546" : "Specified AI Provider not found",
|
|
3327
|
+
404,
|
|
3328
|
+
"PROVIDER_NOT_FOUND"
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
if (supportsOwner) {
|
|
3332
|
+
const isShared = Number(row.is_shared) === 1 || row.owner_id === null;
|
|
3333
|
+
const isOwner = Boolean(userId && row.owner_id === userId);
|
|
3334
|
+
if (!isShared && !isOwner) {
|
|
3335
|
+
throw new AiProviderError(
|
|
3336
|
+
lang === "zh" ? "\u65E0\u6743\u4F7F\u7528\u8BE5 AI \u63D0\u4F9B\u5546\uFF08\u975E\u672C\u4EBA\u79C1\u6709\u4E14\u672A\u5171\u4EAB\uFF09" : "Forbidden: You do not have access to this private AI provider",
|
|
3337
|
+
403,
|
|
3338
|
+
"PROVIDER_ACCESS_DENIED"
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
provider = row;
|
|
3343
|
+
} else {
|
|
3344
|
+
if (!supportsOwner) {
|
|
3345
|
+
provider = db2.prepare(
|
|
3346
|
+
"SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' ORDER BY created_at DESC LIMIT 1"
|
|
3347
|
+
).get();
|
|
3348
|
+
} else if (userId) {
|
|
3349
|
+
provider = db2.prepare(
|
|
3350
|
+
`SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at
|
|
3351
|
+
FROM ai_providers
|
|
3352
|
+
WHERE (owner_id = ? OR is_shared = 1 OR owner_id IS NULL)
|
|
3353
|
+
AND api_key IS NOT NULL AND api_key != ''
|
|
3354
|
+
ORDER BY (CASE WHEN owner_id = ? THEN 0 WHEN is_shared = 1 THEN 1 ELSE 2 END), created_at DESC
|
|
3355
|
+
LIMIT 1`
|
|
3356
|
+
).get(userId, userId);
|
|
3357
|
+
} else {
|
|
3358
|
+
provider = db2.prepare(
|
|
3359
|
+
`SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at
|
|
3360
|
+
FROM ai_providers
|
|
3361
|
+
WHERE (is_shared = 1 OR owner_id IS NULL)
|
|
3362
|
+
AND api_key IS NOT NULL AND api_key != ''
|
|
3363
|
+
ORDER BY (CASE WHEN is_shared = 1 THEN 0 ELSE 1 END), created_at DESC
|
|
3364
|
+
LIMIT 1`
|
|
3365
|
+
).get();
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
if (provider?.api_key) {
|
|
3369
|
+
provider.api_key = decryptApiKey(provider.api_key);
|
|
3370
|
+
}
|
|
3371
|
+
if (!provider || !provider.api_key || !provider.api_key.trim()) {
|
|
3372
|
+
throw new AiProviderError(
|
|
3373
|
+
lang === "zh" ? "\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002" : "No active AI Provider configured. Please add and configure an AI Provider in settings.",
|
|
3374
|
+
400,
|
|
3375
|
+
"NO_AI_PROVIDER"
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
return provider;
|
|
3379
|
+
}
|
|
3380
|
+
var AiProviderError;
|
|
3381
|
+
var init_ai_provider_resolver = __esm({
|
|
3382
|
+
"packages/core/di/ai-provider-resolver.ts"() {
|
|
3383
|
+
"use strict";
|
|
3384
|
+
init_api_key_crypto();
|
|
3385
|
+
AiProviderError = class extends Error {
|
|
3386
|
+
constructor(message, status = 400, code = "AI_PROVIDER_ERROR") {
|
|
3387
|
+
super(message);
|
|
3388
|
+
this.name = "AiProviderError";
|
|
3389
|
+
this.status = status;
|
|
3390
|
+
this.code = code;
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3307
3396
|
// packages/core/di/ai-service.ts
|
|
3308
3397
|
var AIService;
|
|
3309
3398
|
var init_ai_service = __esm({
|
|
@@ -3312,6 +3401,7 @@ var init_ai_service = __esm({
|
|
|
3312
3401
|
init_provider_gateway();
|
|
3313
3402
|
init_ai_event_bus();
|
|
3314
3403
|
init_api_key_crypto();
|
|
3404
|
+
init_ai_provider_resolver();
|
|
3315
3405
|
AIService = class {
|
|
3316
3406
|
constructor(db2) {
|
|
3317
3407
|
this.db = db2;
|
|
@@ -3359,16 +3449,11 @@ var init_ai_service = __esm({
|
|
|
3359
3449
|
this._contextDelegates.unregister?.(id);
|
|
3360
3450
|
}
|
|
3361
3451
|
async generateText(prompt, options) {
|
|
3362
|
-
const provider = this.db
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
}
|
|
3368
|
-
const apiKey = decryptApiKey(provider.api_key);
|
|
3369
|
-
if (!apiKey || !apiKey.trim()) {
|
|
3370
|
-
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");
|
|
3371
|
-
}
|
|
3452
|
+
const provider = resolveAccessibleAiProvider(this.db, {
|
|
3453
|
+
userId: options?.userId,
|
|
3454
|
+
providerId: options?.providerId
|
|
3455
|
+
});
|
|
3456
|
+
const apiKey = provider.api_key;
|
|
3372
3457
|
if (looksLikeCiphertext(apiKey)) {
|
|
3373
3458
|
throw new Error(
|
|
3374
3459
|
"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"
|
|
@@ -9211,6 +9296,7 @@ var init_di = __esm({
|
|
|
9211
9296
|
init_storage_service();
|
|
9212
9297
|
init_ai_service();
|
|
9213
9298
|
init_courseware_runtime_script_registry();
|
|
9299
|
+
init_ai_provider_resolver();
|
|
9214
9300
|
init_container();
|
|
9215
9301
|
}
|
|
9216
9302
|
});
|
|
@@ -13409,8 +13495,8 @@ var ManagementPlugin = {
|
|
|
13409
13495
|
});
|
|
13410
13496
|
await commandBus.registerHandler(classTemplateDownloadCmd, {
|
|
13411
13497
|
async execute() {
|
|
13412
|
-
const headers = "Class Name,Class Desc,Student Name,Student Email";
|
|
13413
|
-
const sampleRow = "Class 101,Introduction to English,John Doe,john@example.com\nClass 101,Introduction to English,Jane Smith,jane@example.com";
|
|
13498
|
+
const headers = "Class Name,Class Desc,Student Number,Student Name,Student Email,Initial Password,Student Notes,Gender";
|
|
13499
|
+
const sampleRow = "Class 101,Introduction to English,20260101,John Doe,john@example.com,,,Male\nClass 101,Introduction to English,20260102,Jane Smith,jane@example.com,,,Female";
|
|
13414
13500
|
const content = `${headers}
|
|
13415
13501
|
${sampleRow}`;
|
|
13416
13502
|
const fileId = (0, import_uuid4.v7)();
|
|
@@ -13442,8 +13528,8 @@ ${sampleRow}`;
|
|
|
13442
13528
|
});
|
|
13443
13529
|
await commandBus.registerHandler(studentTemplateDownloadCmd, {
|
|
13444
13530
|
async execute() {
|
|
13445
|
-
const headers = "Student Name,Student Email";
|
|
13446
|
-
const sampleRow = "Alice Cooper,alice@example.com\
|
|
13531
|
+
const headers = "Student Number,Student Name,Student Email,Initial Password,Student Notes,Gender";
|
|
13532
|
+
const sampleRow = "20260101,Alice Cooper,alice@example.com,,,Male\n20260102,Bob Dylan,bob@example.com,,,Female";
|
|
13447
13533
|
const content = `${headers}
|
|
13448
13534
|
${sampleRow}`;
|
|
13449
13535
|
const fileId = (0, import_uuid4.v7)();
|
|
@@ -21204,7 +21290,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
|
21204
21290
|
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
21205
21291
|
var import_node_url5 = require("node:url");
|
|
21206
21292
|
var import_meta7 = {};
|
|
21207
|
-
var FALLBACK_VERSION = "0.7.
|
|
21293
|
+
var FALLBACK_VERSION = "0.7.13";
|
|
21208
21294
|
function readPlatformVersion() {
|
|
21209
21295
|
const bases = [];
|
|
21210
21296
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
@@ -29897,1298 +29983,366 @@ var ServiceRegistryKernel = class {
|
|
|
29897
29983
|
}
|
|
29898
29984
|
};
|
|
29899
29985
|
|
|
29900
|
-
// packages/core/
|
|
29901
|
-
var
|
|
29902
|
-
|
|
29903
|
-
|
|
29904
|
-
|
|
29905
|
-
|
|
29906
|
-
|
|
29907
|
-
|
|
29908
|
-
|
|
29909
|
-
|
|
29910
|
-
|
|
29911
|
-
|
|
29912
|
-
|
|
29913
|
-
|
|
29914
|
-
|
|
29915
|
-
|
|
29916
|
-
|
|
29917
|
-
|
|
29918
|
-
|
|
29919
|
-
|
|
29986
|
+
// packages/core/kernel/index.ts
|
|
29987
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
29988
|
+
var Kernel = class {
|
|
29989
|
+
/**
|
|
29990
|
+
* @param opts.pluginsDir - 插件产物目录。生产默认 `<cwd>/plugins`;
|
|
29991
|
+
* 测试**必须**显式传入临时目录,否则插件产物会落进工作树。
|
|
29992
|
+
*
|
|
29993
|
+
* ## 为什么要开这个口子(H-1)
|
|
29994
|
+
*
|
|
29995
|
+
* 原本这里硬编码 `path.resolve(process.cwd(), 'plugins')` 且不可覆盖。
|
|
29996
|
+
* 后果不只是「测试会弄脏工作树」,更根本的是 **Kernel 在这个维度上不可测** ——
|
|
29997
|
+
* 没有任何办法在测试里把插件产物引到别处,于是污染一直无人察觉:
|
|
29998
|
+
* 实测仓库 `plugins/` 下积累了 **1705 个孤儿目录 / 24MB**,且每次 `pnpm test` 继续增长。
|
|
29999
|
+
* 该目录被 `.gitignore` 忽略,所以 `git status` 完全看不见。
|
|
30000
|
+
*
|
|
30001
|
+
* 加可选参数后,生产行为**逐字节不变**(默认值相同),而测试可把产物引到
|
|
30002
|
+
* `os.tmpdir()`。`vitest.setup.ts` 的全局守卫会把「仍写工作树」变成硬失败,
|
|
30003
|
+
* 防止将来有人再引入同类调用。
|
|
30004
|
+
*/
|
|
30005
|
+
constructor(opts) {
|
|
30006
|
+
this.db = db;
|
|
30007
|
+
this.readPool = readPool;
|
|
30008
|
+
this.getReadDb = getReadDb;
|
|
30009
|
+
this.queryRead = queryRead;
|
|
30010
|
+
this.queryReadOne = queryReadOne;
|
|
30011
|
+
this.checkpoint = checkpoint;
|
|
30012
|
+
this.eventBus = new EventBus();
|
|
30013
|
+
this.capabilityGuard = new CapabilityGuard();
|
|
30014
|
+
this.serviceRegistry = new ServiceRegistry();
|
|
30015
|
+
this.storageService = new StorageService(this.db);
|
|
30016
|
+
this.aiService = new AIService(this.db);
|
|
30017
|
+
this.aiRuntime = new AIRuntimeKernel();
|
|
30018
|
+
this.aiCapability = new AICapabilityKernel(this.aiRuntime);
|
|
30019
|
+
this.capabilityFrameworkRuntime = new CapabilityRuntimeKernel(this.aiCapability);
|
|
30020
|
+
this.capabilityGovernance = new CapabilityGovernanceKernel();
|
|
30021
|
+
this.platformServiceRegistryKernel = new ServiceRegistryKernel();
|
|
30022
|
+
this.analyticsEngine = new AnalyticsEngineKernel();
|
|
30023
|
+
this.collaborationEngine = new CollaborationEngineKernel();
|
|
30024
|
+
this.presenceEngine = new PresenceEngineKernel();
|
|
30025
|
+
this.classroomRuntime = new ClassroomRuntimeKernel();
|
|
30026
|
+
this.lessonRuntime = new LessonRuntime({ eventBus: this.eventBus });
|
|
30027
|
+
this.lessonRuntime.aiInterface.setAIService(this.aiService);
|
|
30028
|
+
this.commandBus = new CommandBus(this.eventBus);
|
|
30029
|
+
this.actionRegistry = new ActionRegistry();
|
|
30030
|
+
this.processManager = new ProcessManager(this);
|
|
30031
|
+
this.esmLoader = new NodeEsmLoader();
|
|
30032
|
+
const pluginsDir = opts?.pluginsDir ?? import_path12.default.resolve(process.cwd(), "plugins");
|
|
30033
|
+
try {
|
|
30034
|
+
import_fs13.default.mkdirSync(pluginsDir, { recursive: true });
|
|
30035
|
+
} catch (err) {
|
|
30036
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
30037
|
+
throw new Error(
|
|
30038
|
+
`[Kernel] FATAL: \u65E0\u6CD5\u521B\u5EFA\u63D2\u4EF6\u76EE\u5F55 ${pluginsDir}\uFF08${detail}\uFF09\u3002\u5BB9\u5668\u8BF7\u628A OPENLEARN_DATA_DIR \u6216 OPENLEARN_PLUGINS_DIR \u6307\u5230\u53EF\u5199\u5377\uFF0C\u4F8B\u5982 /data/plugins\u3002`
|
|
30039
|
+
);
|
|
29920
30040
|
}
|
|
29921
|
-
|
|
29922
|
-
|
|
29923
|
-
|
|
29924
|
-
|
|
29925
|
-
|
|
29926
|
-
|
|
29927
|
-
|
|
29928
|
-
|
|
29929
|
-
|
|
29930
|
-
|
|
29931
|
-
|
|
29932
|
-
|
|
29933
|
-
|
|
29934
|
-
|
|
30041
|
+
this.pluginHost = new PluginHost(this.serviceRegistry, this.esmLoader, this.db, pluginsDir);
|
|
30042
|
+
setPluginNamespaceResolver((namespace) => this.pluginHost.findByManifestId(namespace) ? namespace : void 0);
|
|
30043
|
+
this.workerManager = new WorkerManager(this.serviceRegistry, this.capabilityGuard, this.db);
|
|
30044
|
+
this.pluginHost.setWorkerManager(this.workerManager);
|
|
30045
|
+
const capabilityRegistry = this.aiCapability.registry;
|
|
30046
|
+
this.pluginRuntimeComposition = new PluginRuntimeComposition(this.pluginHost, this.workerManager);
|
|
30047
|
+
this.pluginLifecycleManager = new PluginLifecycleManager(this.pluginHost);
|
|
30048
|
+
this.pluginDistributionManager = new PluginDistributionManager(this.pluginHost);
|
|
30049
|
+
this.unifiedExtensionRegistry = new UnifiedExtensionRegistry();
|
|
30050
|
+
this.capabilityRegistry = capabilityRegistry;
|
|
30051
|
+
this.pluginCapabilityGateway = new PluginCapabilityGateway(capabilityRegistry);
|
|
30052
|
+
this.serviceRegistry.register(IEventBusServiceToken3, this.eventBus);
|
|
30053
|
+
this.serviceRegistry.register(ICapabilityServiceToken, this.capabilityGuard);
|
|
30054
|
+
this.serviceRegistry.register(IStorageServiceToken, this.storageService);
|
|
30055
|
+
this.serviceRegistry.register(ICommandBusServiceToken4, this.commandBus);
|
|
30056
|
+
this.serviceRegistry.register(IActionRegistryServiceToken4, this.actionRegistry);
|
|
30057
|
+
this.serviceRegistry.register(IProcessServiceToken2, this.processManager);
|
|
30058
|
+
this.serviceRegistry.register(IAIServiceToken, this.aiService);
|
|
30059
|
+
this.serviceRegistry.register(IDatabaseToken4, this.db);
|
|
30060
|
+
this.serviceRegistry.register(IPluginHostToken, this.pluginHost);
|
|
30061
|
+
this.serviceRegistry.register(IPluginLifecycleManagerToken, this.pluginLifecycleManager);
|
|
30062
|
+
this.serviceRegistry.register(IPluginDistributionManagerToken, this.pluginDistributionManager);
|
|
30063
|
+
this.serviceRegistry.register(IPluginRuntimeCompositionToken, this.pluginRuntimeComposition);
|
|
30064
|
+
this.serviceRegistry.register(IUnifiedExtensionRegistryToken, this.unifiedExtensionRegistry);
|
|
30065
|
+
this.serviceRegistry.register(IPluginCapabilityGatewayToken, this.pluginCapabilityGateway);
|
|
30066
|
+
this.serviceRegistry.register(ICapabilityRegistryToken, this.capabilityRegistry);
|
|
30067
|
+
this.serviceRegistry.register(ISemesterGradeServiceToken, new SemesterGradeService(this.db));
|
|
30068
|
+
this.serviceRegistry.register(IPointsDimensionRegistryToken, new PointsDimensionRegistry());
|
|
30069
|
+
this.serviceRegistry.register(IPointsLedgerServiceToken, new PointsLedgerService(this.db));
|
|
30070
|
+
this.serviceRegistry.register(IClassRosterServiceToken, new ClassRosterService(this.db));
|
|
30071
|
+
this.serviceRegistry.register(ILessonEngineServiceToken, { getRuntime: async () => this.lessonRuntime });
|
|
30072
|
+
this.serviceRegistry.register(IClassroomRuntimeServiceToken, {
|
|
30073
|
+
getRuntimeKernel: async () => this.classroomRuntime
|
|
30074
|
+
});
|
|
30075
|
+
this.serviceRegistry.register(IPresenceEngineServiceToken, {
|
|
30076
|
+
getPresenceEngine: async () => this.presenceEngine
|
|
30077
|
+
});
|
|
30078
|
+
this.serviceRegistry.register(ITeachingCollaborationServiceToken, {
|
|
30079
|
+
getCollaborationEngine: async () => this.collaborationEngine
|
|
30080
|
+
});
|
|
30081
|
+
this.serviceRegistry.register(ILearningAnalyticsServiceToken, {
|
|
30082
|
+
getAnalyticsEngine: async () => this.analyticsEngine
|
|
30083
|
+
});
|
|
30084
|
+
this.serviceRegistry.register(IAICapabilityServiceToken, {
|
|
30085
|
+
getCapabilityKernel: async () => this.aiCapability
|
|
30086
|
+
});
|
|
30087
|
+
this.serviceRegistry.register(ICapabilityRuntimeServiceToken, {
|
|
30088
|
+
getRuntimeKernel: async () => this.capabilityFrameworkRuntime
|
|
30089
|
+
});
|
|
30090
|
+
this.serviceRegistry.register(ICapabilityGovernanceServiceToken, {
|
|
30091
|
+
getGovernanceKernel: async () => this.capabilityGovernance
|
|
30092
|
+
});
|
|
30093
|
+
this.serviceRegistry.register(IPlatformServiceRegistryToken, {
|
|
30094
|
+
getServiceRegistryKernel: async () => this.platformServiceRegistryKernel
|
|
30095
|
+
});
|
|
30096
|
+
this.serviceRegistry.register(IAuthSessionBridgeToken, new AuthSessionBridgeService(this.db));
|
|
30097
|
+
this.serviceRegistry.register(ICoursewareRuntimeScriptRegistryToken, new CoursewareRuntimeScriptRegistry());
|
|
30098
|
+
this.serviceRegistry.register(IStageGuardServiceToken, defaultStageGuardPipeline);
|
|
30099
|
+
this.serviceRegistry.register(IAssessmentGraderRegistryToken, assessmentGraderRegistry);
|
|
30100
|
+
this.commandBus.setInterceptor(async (command) => {
|
|
30101
|
+
const action = this.actionRegistry.getActionByCommandType(command.type);
|
|
30102
|
+
if (action) {
|
|
30103
|
+
if (action.inputSchema) {
|
|
30104
|
+
const errors = validateJsonSchema(command.payload, action.inputSchema);
|
|
30105
|
+
if (errors.length > 0) {
|
|
30106
|
+
throw new Error(
|
|
30107
|
+
`[PayloadValidationError] Invalid command payload for ${command.type}: ${errors.join("; ")}`
|
|
30108
|
+
);
|
|
30109
|
+
}
|
|
30110
|
+
}
|
|
30111
|
+
const isAdmin = command.actorId === "role:administrator" || command.actorId === "admin" || command.actorId === "usr_admin" || command.actorId === "admin-demo" || typeof command.actorId === "string" && command.actorId.startsWith("user:") && (command.actorId.endsWith(":administrator") || command.actorId.endsWith(":admin"));
|
|
30112
|
+
if (action.capabilityRequired && !isAdmin) {
|
|
30113
|
+
const allowed = this.capabilityGuard.check(command.actorId, action.capabilityRequired);
|
|
30114
|
+
if (!allowed) {
|
|
30115
|
+
const err = new Error(
|
|
30116
|
+
`[CapabilityGuard] Access Denied: Actor ${command.actorId} missing capability ${action.capabilityRequired} for ${command.type}`
|
|
30117
|
+
);
|
|
30118
|
+
err.status = 403;
|
|
30119
|
+
err.code = "COMMAND_CAPABILITY_DENIED";
|
|
30120
|
+
throw err;
|
|
30121
|
+
}
|
|
30122
|
+
}
|
|
30123
|
+
const pluginSuppliedApproval = typeof command.actorId === "string" && command.actorId.startsWith("plugin:");
|
|
30124
|
+
if (action.isHighRisk && (command.metadata?.approved !== true || pluginSuppliedApproval)) {
|
|
30125
|
+
if (isAdmin && !pluginSuppliedApproval) {
|
|
30126
|
+
console.log(
|
|
30127
|
+
`[Security] Command ${command.type} initiated by Administrator (${command.actorId}). Bypassing human approval.`
|
|
30128
|
+
);
|
|
30129
|
+
} else {
|
|
30130
|
+
const stmt = this.db.prepare(
|
|
30131
|
+
"INSERT INTO pending_commands (id, command_type, payload, actor_id, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
30132
|
+
);
|
|
30133
|
+
stmt.run(command.id, command.type, JSON.stringify(command.payload), command.actorId, Date.now());
|
|
30134
|
+
this.eventBus.publish({
|
|
30135
|
+
id: (0, import_uuid15.v7)(),
|
|
30136
|
+
type: "approval.requested",
|
|
30137
|
+
source: "kernel.security",
|
|
30138
|
+
payload: { commandId: command.id, commandType: command.type },
|
|
30139
|
+
timestamp: Date.now(),
|
|
30140
|
+
correlationId: command.id
|
|
30141
|
+
});
|
|
30142
|
+
throw new Error(
|
|
30143
|
+
`[Security] Command ${command.type} requires human approval. It has been queued to pending actions.`
|
|
30144
|
+
);
|
|
30145
|
+
}
|
|
30146
|
+
}
|
|
29935
30147
|
} else {
|
|
29936
|
-
|
|
30148
|
+
const err = new Error(
|
|
30149
|
+
`[Security] Command "${command.type}" has no registered action descriptor and is denied by default. Only commands registered via actionRegistry.register() may be dispatched. If you own this command, register an ActionDescriptor (capabilityRequired + inputSchema).`
|
|
30150
|
+
);
|
|
30151
|
+
err.status = 403;
|
|
30152
|
+
err.code = "COMMAND_NOT_REGISTERED";
|
|
30153
|
+
throw err;
|
|
30154
|
+
}
|
|
30155
|
+
});
|
|
30156
|
+
Promise.resolve().then(() => (init_context_builder(), context_builder_exports)).then((m) => m.bootstrapSharedModules()).catch((err) => {
|
|
30157
|
+
console.warn("[Kernel] Failed to bootstrap shared modules:", err.message);
|
|
30158
|
+
});
|
|
30159
|
+
this.ready = this.bootstrapSystemPlugins().catch((err) => {
|
|
30160
|
+
console.error("[Kernel] Critical system plugin bootstrap failed:", err);
|
|
30161
|
+
process.exit(1);
|
|
30162
|
+
});
|
|
30163
|
+
if (process.env.NODE_ENV === "development") {
|
|
30164
|
+
const watchDir = this.pluginHost.getPluginsDir();
|
|
30165
|
+
try {
|
|
30166
|
+
const hotReload = new HotReloadController(this.pluginHost, watchDir);
|
|
30167
|
+
this.pluginHost.setHotReloadController(hotReload);
|
|
30168
|
+
hotReload.start().catch((err) => {
|
|
30169
|
+
console.warn("[Kernel] Hot reload initialization failed:", err.message);
|
|
30170
|
+
});
|
|
30171
|
+
} catch (err) {
|
|
30172
|
+
console.warn("[Kernel] Hot reload initialization failed:", err.message);
|
|
29937
30173
|
}
|
|
29938
30174
|
}
|
|
29939
|
-
return {
|
|
29940
|
-
isValid: errors.length === 0,
|
|
29941
|
-
errors: Object.freeze(errors),
|
|
29942
|
-
warnings: Object.freeze(warnings)
|
|
29943
|
-
};
|
|
29944
30175
|
}
|
|
29945
|
-
|
|
29946
|
-
|
|
29947
|
-
|
|
29948
|
-
|
|
29949
|
-
|
|
29950
|
-
|
|
29951
|
-
|
|
29952
|
-
|
|
29953
|
-
|
|
29954
|
-
|
|
29955
|
-
|
|
29956
|
-
|
|
29957
|
-
|
|
30176
|
+
async bootstrapSystemPlugins() {
|
|
30177
|
+
await this.migratePluginsToFilesystem();
|
|
30178
|
+
const systemPlugins = [
|
|
30179
|
+
{ id: "@openlearn/plugin-vfs", mod: VfsPlugin, name: "Virtual File System Plugin", critical: true },
|
|
30180
|
+
{ id: "@openlearn/plugin-process", mod: ProcessPlugin, name: "Background Process Plugin", critical: true },
|
|
30181
|
+
{ id: "@openlearn/plugin-management", mod: ManagementPlugin, name: "LMS Management Plugin", critical: true },
|
|
30182
|
+
{ id: "@openlearn/plugin-builtin", mod: BuiltinPlugin, name: "Classroom Builtin Plugin", critical: true },
|
|
30183
|
+
{ id: "@openlearn/plugin-ai-planner", mod: AiPlannerPlugin, name: "AI Planner Plugin", critical: false },
|
|
30184
|
+
{
|
|
30185
|
+
id: "@openlearn/plugin-ai-submit-injector",
|
|
30186
|
+
mod: AiSubmitInjectorPlugin,
|
|
30187
|
+
name: "AI Submit Injector Plugin",
|
|
30188
|
+
critical: false
|
|
30189
|
+
},
|
|
30190
|
+
{
|
|
30191
|
+
id: "@openlearn/plugin-assignment-eval",
|
|
30192
|
+
mod: AssignmentEvalPlugin,
|
|
30193
|
+
name: "Assignment Evaluation and Peer Review Plugin",
|
|
30194
|
+
critical: false
|
|
30195
|
+
}
|
|
30196
|
+
];
|
|
30197
|
+
for (const plugin of systemPlugins) {
|
|
30198
|
+
try {
|
|
30199
|
+
let row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(plugin.id);
|
|
30200
|
+
if (!row) {
|
|
30201
|
+
this.db.prepare(
|
|
30202
|
+
"INSERT INTO plugins (id, name, manifest, source_code, file_path, status, created_at, loader_version, execution_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
30203
|
+
).run(
|
|
30204
|
+
plugin.id,
|
|
30205
|
+
plugin.name,
|
|
30206
|
+
JSON.stringify(plugin.mod.manifest),
|
|
30207
|
+
"",
|
|
30208
|
+
// source_code: system plugins are preloaded in-memory
|
|
30209
|
+
null,
|
|
30210
|
+
// file_path: system plugins have no file
|
|
30211
|
+
"installed",
|
|
30212
|
+
Date.now(),
|
|
30213
|
+
"esm",
|
|
30214
|
+
"inline"
|
|
30215
|
+
);
|
|
30216
|
+
}
|
|
30217
|
+
this.pluginHost.registerPreloadedPlugin(
|
|
30218
|
+
plugin.id,
|
|
30219
|
+
plugin.mod
|
|
30220
|
+
);
|
|
30221
|
+
await this.pluginHost.activatePlugin(plugin.id);
|
|
30222
|
+
} catch (err) {
|
|
30223
|
+
if (plugin.critical) {
|
|
30224
|
+
console.error(`[Kernel] Failed to bootstrap critical system plugin ${plugin.name}:`, err);
|
|
30225
|
+
throw err;
|
|
30226
|
+
} else {
|
|
30227
|
+
console.warn(`[Kernel] Soft-fail: Failed to bootstrap AI system plugin ${plugin.name}:`, err);
|
|
30228
|
+
}
|
|
30229
|
+
}
|
|
29958
30230
|
}
|
|
29959
|
-
|
|
29960
|
-
|
|
29961
|
-
|
|
29962
|
-
|
|
29963
|
-
field: "port"
|
|
29964
|
-
});
|
|
30231
|
+
try {
|
|
30232
|
+
await this.pluginHost.restoreActivePlugins();
|
|
30233
|
+
} catch (err) {
|
|
30234
|
+
console.error("[Kernel] Failed to restore active plugins:", err);
|
|
29965
30235
|
}
|
|
29966
|
-
|
|
29967
|
-
|
|
29968
|
-
|
|
29969
|
-
|
|
29970
|
-
|
|
29971
|
-
|
|
29972
|
-
|
|
29973
|
-
|
|
29974
|
-
|
|
29975
|
-
|
|
29976
|
-
|
|
29977
|
-
|
|
29978
|
-
|
|
29979
|
-
|
|
29980
|
-
|
|
29981
|
-
|
|
29982
|
-
|
|
29983
|
-
|
|
29984
|
-
|
|
29985
|
-
|
|
29986
|
-
|
|
29987
|
-
|
|
30236
|
+
}
|
|
30237
|
+
/**
|
|
30238
|
+
* Phase 7: 将 DB 中存有 source_code 的旧插件迁移到文件系统(幂等)
|
|
30239
|
+
*/
|
|
30240
|
+
async migratePluginsToFilesystem() {
|
|
30241
|
+
const plugins = this.db.prepare("SELECT id, source_code, manifest FROM plugins WHERE source_code != '' AND source_code IS NOT NULL").all();
|
|
30242
|
+
if (plugins.length === 0) return;
|
|
30243
|
+
console.log(`[Migration] Found ${plugins.length} plugin(s) to migrate to filesystem`);
|
|
30244
|
+
const pluginsDir = this.pluginHost.getPluginsDir();
|
|
30245
|
+
for (const p of plugins) {
|
|
30246
|
+
const pluginDir = import_path12.default.join(pluginsDir, p.id);
|
|
30247
|
+
const indexPath = import_path12.default.join(pluginDir, "index.js");
|
|
30248
|
+
const manifestPath = import_path12.default.join(pluginDir, "manifest.json");
|
|
30249
|
+
if (import_fs13.default.existsSync(indexPath)) {
|
|
30250
|
+
this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
|
|
30251
|
+
continue;
|
|
30252
|
+
}
|
|
30253
|
+
try {
|
|
30254
|
+
import_fs13.default.mkdirSync(pluginDir, { recursive: true });
|
|
30255
|
+
import_fs13.default.writeFileSync(indexPath, p.source_code, "utf-8");
|
|
30256
|
+
if (!import_fs13.default.existsSync(manifestPath)) {
|
|
30257
|
+
import_fs13.default.writeFileSync(manifestPath, p.manifest, "utf-8");
|
|
29988
30258
|
}
|
|
30259
|
+
this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
|
|
30260
|
+
console.log(`[Migration] Plugin "${p.id}" migrated to ${indexPath}`);
|
|
30261
|
+
} catch (err) {
|
|
30262
|
+
console.error(`[Migration] Failed to migrate plugin "${p.id}":`, err);
|
|
29989
30263
|
}
|
|
29990
30264
|
}
|
|
29991
|
-
|
|
29992
|
-
isValid: errors.length === 0,
|
|
29993
|
-
errors: Object.freeze(errors),
|
|
29994
|
-
warnings: Object.freeze(warnings)
|
|
29995
|
-
};
|
|
30265
|
+
console.log("[Migration] Plugin migration complete");
|
|
29996
30266
|
}
|
|
29997
|
-
|
|
29998
|
-
|
|
29999
|
-
|
|
30000
|
-
|
|
30001
|
-
|
|
30002
|
-
|
|
30003
|
-
|
|
30004
|
-
|
|
30005
|
-
|
|
30267
|
+
// Subscribe to all events and log them to DB
|
|
30268
|
+
initAuditLog() {
|
|
30269
|
+
let stmt = null;
|
|
30270
|
+
let hasLessonIdColumn = null;
|
|
30271
|
+
this.eventBus.subscribe("*", (event) => {
|
|
30272
|
+
const values = [
|
|
30273
|
+
event.id,
|
|
30274
|
+
event.type,
|
|
30275
|
+
event.source,
|
|
30276
|
+
JSON.stringify(redactPasscodeMaterial(event.payload)),
|
|
30277
|
+
event.timestamp,
|
|
30278
|
+
event.correlationId || null
|
|
30279
|
+
];
|
|
30280
|
+
if (hasLessonIdColumn === null) {
|
|
30281
|
+
const columns = this.db.prepare("PRAGMA table_info(events)").all();
|
|
30282
|
+
hasLessonIdColumn = columns.some((c) => c.name === "lesson_id");
|
|
30283
|
+
stmt = this.db.prepare(hasLessonIdColumn ? INSERT_EVENT_WITH_LESSON : INSERT_EVENT_LEGACY);
|
|
30284
|
+
}
|
|
30285
|
+
if (hasLessonIdColumn) {
|
|
30286
|
+
stmt.run(...values, extractEventLessonId(event.payload));
|
|
30287
|
+
} else {
|
|
30288
|
+
stmt.run(...values);
|
|
30289
|
+
}
|
|
30290
|
+
});
|
|
30006
30291
|
}
|
|
30007
30292
|
};
|
|
30008
|
-
var
|
|
30009
|
-
|
|
30010
|
-
|
|
30011
|
-
|
|
30293
|
+
var INSERT_EVENT_WITH_LESSON = "INSERT INTO events (id, type, source, payload, timestamp, correlationId, lesson_id) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
30294
|
+
var INSERT_EVENT_LEGACY = "INSERT INTO events (id, type, source, payload, timestamp, correlationId) VALUES (?, ?, ?, ?, ?, ?)";
|
|
30295
|
+
function extractEventLessonId(payload) {
|
|
30296
|
+
if (!payload || typeof payload !== "object") return null;
|
|
30297
|
+
const record = payload;
|
|
30298
|
+
for (const key of ["lessonId", "lesson_id"]) {
|
|
30299
|
+
const value = record[key];
|
|
30300
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
30012
30301
|
}
|
|
30013
|
-
|
|
30014
|
-
|
|
30015
|
-
|
|
30016
|
-
|
|
30017
|
-
|
|
30302
|
+
return null;
|
|
30303
|
+
}
|
|
30304
|
+
function resolvePluginsDirOverride() {
|
|
30305
|
+
const raw = process.env.OPENLEARN_PLUGINS_DIR;
|
|
30306
|
+
if (typeof raw === "string" && raw.length > 0) return raw;
|
|
30307
|
+
const dataDir = process.env.OPENLEARN_DATA_DIR?.trim();
|
|
30308
|
+
if (dataDir) return import_path12.default.join(import_path12.default.resolve(dataDir), "plugins");
|
|
30309
|
+
if (process.env.VITEST) {
|
|
30310
|
+
throw new Error(
|
|
30311
|
+
"[H-1] \u6D4B\u8BD5\u73AF\u5883\u4E0B\u672A\u80FD\u786E\u5B9A\u63D2\u4EF6\u76EE\u5F55\uFF0C\u62D2\u7EDD\u56DE\u9000\u5230 process.cwd()/plugins\u3002\n \u539F\u56E0\uFF1A\u56DE\u9000\u4F1A\u628A\u63D2\u4EF6\u4EA7\u7269\u5199\u8FDB\u4ED3\u5E93\u5DE5\u4F5C\u6811\uFF1B\u8BE5\u76EE\u5F55\u88AB .gitignore \u5FFD\u7565\uFF0C\u6C61\u67D3\u5B8C\u5168\u9690\u5F62\u3002\n \u4FEE\u6CD5\uFF08\u62E9\u4E00\uFF09\uFF1A\n 1. \u8BE5\u6D4B\u8BD5\u81EA\u5DF1 new Kernel({ pluginsDir })\uFF0C\u7528 __tests__/helpers/plugins-dir \u7684\n createPluginsDir('\u7528\u9014') \u53D6\u5F97\uFF0C\u5E76\u5728 afterEach \u91CC cleanupPluginsDir\uFF1B\n 2. \u82E5\u7528\u7684\u662F kernelContainer \u5355\u4F8B\uFF0C\u786E\u8BA4\u5B83\u662F\u5728 vitest.setup.ts \u8D4B\u503C\n OPENLEARN_PLUGINS_DIR \u4E4B\u540E\u624D\u88AB\u9996\u6B21\u8BBF\u95EE\u3002"
|
|
30312
|
+
);
|
|
30018
30313
|
}
|
|
30019
|
-
|
|
30020
|
-
|
|
30021
|
-
var
|
|
30022
|
-
|
|
30023
|
-
|
|
30024
|
-
|
|
30025
|
-
|
|
30026
|
-
"Activating" /* Activating */,
|
|
30027
|
-
"Ready" /* Ready */,
|
|
30028
|
-
"ShuttingDown" /* ShuttingDown */,
|
|
30029
|
-
"Disposed" /* Disposed */
|
|
30030
|
-
]);
|
|
30031
|
-
var DEFAULT_BOOTSTRAP_CONFIG = Object.freeze({
|
|
30032
|
-
environment: "development",
|
|
30033
|
-
mode: "standalone",
|
|
30034
|
-
port: 9e3,
|
|
30035
|
-
debug: false,
|
|
30036
|
-
pluginsDir: "./plugins",
|
|
30037
|
-
dbPath: "./packages/core/db/educational_os.db"
|
|
30038
|
-
});
|
|
30039
|
-
|
|
30040
|
-
// packages/core/bootstrap/pipeline/pipeline-executor.ts
|
|
30041
|
-
var PipelineExecutor = class {
|
|
30042
|
-
constructor() {
|
|
30043
|
-
this.listeners = /* @__PURE__ */ new Set();
|
|
30314
|
+
return void 0;
|
|
30315
|
+
}
|
|
30316
|
+
var _kernelContainer;
|
|
30317
|
+
function ensureKernelContainer() {
|
|
30318
|
+
if (!_kernelContainer) {
|
|
30319
|
+
_kernelContainer = new Kernel({ pluginsDir: resolvePluginsDirOverride() });
|
|
30320
|
+
_kernelContainer.initAuditLog();
|
|
30044
30321
|
}
|
|
30045
|
-
|
|
30046
|
-
|
|
30047
|
-
|
|
30048
|
-
|
|
30049
|
-
|
|
30322
|
+
return _kernelContainer;
|
|
30323
|
+
}
|
|
30324
|
+
var kernelContainer = new Proxy({}, {
|
|
30325
|
+
get(target, prop, receiver) {
|
|
30326
|
+
return Reflect.get(ensureKernelContainer(), prop, receiver);
|
|
30327
|
+
},
|
|
30328
|
+
set(target, prop, value, receiver) {
|
|
30329
|
+
return Reflect.set(ensureKernelContainer(), prop, value, receiver);
|
|
30050
30330
|
}
|
|
30051
|
-
|
|
30052
|
-
|
|
30053
|
-
|
|
30054
|
-
|
|
30055
|
-
|
|
30056
|
-
|
|
30057
|
-
|
|
30058
|
-
|
|
30059
|
-
|
|
30060
|
-
|
|
30061
|
-
|
|
30062
|
-
|
|
30063
|
-
|
|
30064
|
-
|
|
30065
|
-
|
|
30066
|
-
for (const stage of stages) {
|
|
30067
|
-
if (context.isCancelled) {
|
|
30068
|
-
console.warn(`[Platform] Pipeline cancelled before stage ${stage.name}`);
|
|
30069
|
-
const totalDurationMs2 = Date.now() - startTime;
|
|
30070
|
-
return {
|
|
30071
|
-
totalDurationMs: totalDurationMs2,
|
|
30072
|
-
status: "Aborted",
|
|
30073
|
-
stageResults: Object.freeze(results)
|
|
30074
|
-
};
|
|
30075
|
-
}
|
|
30076
|
-
console.log(`[Platform] Entering ${stage.name}`);
|
|
30077
|
-
this.emit({
|
|
30078
|
-
type: "StageStarted",
|
|
30079
|
-
timestamp: Date.now(),
|
|
30080
|
-
stageId: stage.id,
|
|
30081
|
-
stageName: String(stage.name)
|
|
30082
|
-
});
|
|
30083
|
-
const stageStart = Date.now();
|
|
30084
|
-
try {
|
|
30085
|
-
if (stage.timeoutMs && stage.timeoutMs > 0) {
|
|
30086
|
-
await this.executeWithTimeout(stage, context, stage.timeoutMs);
|
|
30087
|
-
} else {
|
|
30088
|
-
await stage.execute(context);
|
|
30089
|
-
}
|
|
30090
|
-
const durationMs = Date.now() - stageStart;
|
|
30091
|
-
console.log(`[Platform] Completed ${stage.name} (${durationMs} ms)`);
|
|
30092
|
-
results.push({
|
|
30093
|
-
stageId: stage.id,
|
|
30094
|
-
stageName: String(stage.name),
|
|
30095
|
-
durationMs,
|
|
30096
|
-
status: "Success"
|
|
30097
|
-
});
|
|
30098
|
-
executedStages.push(stage);
|
|
30099
|
-
this.emit({
|
|
30100
|
-
type: "StageCompleted",
|
|
30101
|
-
timestamp: Date.now(),
|
|
30102
|
-
stageId: stage.id,
|
|
30103
|
-
stageName: String(stage.name),
|
|
30104
|
-
durationMs
|
|
30105
|
-
});
|
|
30106
|
-
} catch (err) {
|
|
30107
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
30108
|
-
const durationMs = Date.now() - stageStart;
|
|
30109
|
-
console.error(`[Platform] Failed ${stage.name} (${durationMs} ms): ${error.message}`);
|
|
30110
|
-
results.push({
|
|
30111
|
-
stageId: stage.id,
|
|
30112
|
-
stageName: String(stage.name),
|
|
30113
|
-
durationMs,
|
|
30114
|
-
status: "Failed",
|
|
30115
|
-
error
|
|
30116
|
-
});
|
|
30117
|
-
this.emit({
|
|
30118
|
-
type: "StageFailed",
|
|
30119
|
-
timestamp: Date.now(),
|
|
30120
|
-
stageId: stage.id,
|
|
30121
|
-
stageName: String(stage.name),
|
|
30122
|
-
durationMs,
|
|
30123
|
-
error
|
|
30124
|
-
});
|
|
30125
|
-
await this.rollbackExecutedStages(executedStages, context);
|
|
30126
|
-
const totalDurationMs2 = Date.now() - startTime;
|
|
30127
|
-
const pipelineResult = {
|
|
30128
|
-
totalDurationMs: totalDurationMs2,
|
|
30129
|
-
status: "Failed",
|
|
30130
|
-
stageResults: Object.freeze(results),
|
|
30131
|
-
failedStage: stage.id,
|
|
30132
|
-
error
|
|
30133
|
-
};
|
|
30134
|
-
this.emit({
|
|
30135
|
-
type: "PipelineCompleted",
|
|
30136
|
-
timestamp: Date.now(),
|
|
30137
|
-
durationMs: totalDurationMs2,
|
|
30138
|
-
error
|
|
30139
|
-
});
|
|
30140
|
-
return pipelineResult;
|
|
30141
|
-
}
|
|
30142
|
-
}
|
|
30143
|
-
const totalDurationMs = Date.now() - startTime;
|
|
30144
|
-
console.log(`[Platform] Ready. Total startup duration: ${totalDurationMs} ms.`);
|
|
30145
|
-
const finalResult = {
|
|
30146
|
-
totalDurationMs,
|
|
30147
|
-
status: "Success",
|
|
30148
|
-
stageResults: Object.freeze(results)
|
|
30149
|
-
};
|
|
30150
|
-
this.emit({
|
|
30151
|
-
type: "PipelineCompleted",
|
|
30152
|
-
timestamp: Date.now(),
|
|
30153
|
-
durationMs: totalDurationMs
|
|
30154
|
-
});
|
|
30155
|
-
return finalResult;
|
|
30156
|
-
}
|
|
30157
|
-
async executeWithTimeout(stage, context, timeoutMs) {
|
|
30158
|
-
let timer;
|
|
30159
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
30160
|
-
timer = setTimeout(() => {
|
|
30161
|
-
reject(new StartupTimeoutError(`Stage execution timed out after ${timeoutMs} ms`));
|
|
30162
|
-
}, timeoutMs);
|
|
30163
|
-
});
|
|
30164
|
-
try {
|
|
30165
|
-
await Promise.race([stage.execute(context), timeoutPromise]);
|
|
30166
|
-
} finally {
|
|
30167
|
-
if (timer) {
|
|
30168
|
-
clearTimeout(timer);
|
|
30169
|
-
}
|
|
30170
|
-
}
|
|
30171
|
-
}
|
|
30172
|
-
async rollbackExecutedStages(executedStages, context) {
|
|
30173
|
-
console.log("[Platform] Triggering stage rollbacks...");
|
|
30174
|
-
for (let i = executedStages.length - 1; i >= 0; i--) {
|
|
30175
|
-
const stage = executedStages[i];
|
|
30176
|
-
if (stage.rollback) {
|
|
30177
|
-
try {
|
|
30178
|
-
console.log(`[Platform] Rolling back ${stage.name}`);
|
|
30179
|
-
await stage.rollback(context);
|
|
30180
|
-
} catch (rbErr) {
|
|
30181
|
-
console.error(`[Platform] Error during rollback of ${stage.name}:`, rbErr);
|
|
30182
|
-
}
|
|
30183
|
-
}
|
|
30184
|
-
}
|
|
30185
|
-
}
|
|
30186
|
-
};
|
|
30187
|
-
|
|
30188
|
-
// packages/core/bootstrap/pipeline/stages/standard-stages.ts
|
|
30189
|
-
var StartupStageImpl = class {
|
|
30190
|
-
constructor() {
|
|
30191
|
-
this.id = "stage_startup";
|
|
30192
|
-
this.name = "Created" /* Created */;
|
|
30193
|
-
this.description = "Environment & configuration validation";
|
|
30194
|
-
}
|
|
30195
|
-
async execute(context) {
|
|
30196
|
-
context.setStage("Configuring" /* Configuring */);
|
|
30197
|
-
}
|
|
30198
|
-
};
|
|
30199
|
-
var RegistrationStageImpl = class {
|
|
30200
|
-
constructor() {
|
|
30201
|
-
this.id = "stage_registration";
|
|
30202
|
-
this.name = "Registering" /* Registering */;
|
|
30203
|
-
this.description = "Core Service Contracts & DI Tokens registration";
|
|
30204
|
-
}
|
|
30205
|
-
async execute(context) {
|
|
30206
|
-
context.setStage("Registering" /* Registering */);
|
|
30207
|
-
}
|
|
30208
|
-
};
|
|
30209
|
-
var InitializationStageImpl = class {
|
|
30210
|
-
constructor() {
|
|
30211
|
-
this.id = "stage_initialization";
|
|
30212
|
-
this.name = "Initializing" /* Initializing */;
|
|
30213
|
-
this.description = "Subsystem Kernels initialization";
|
|
30214
|
-
}
|
|
30215
|
-
async execute(context) {
|
|
30216
|
-
context.setStage("Initializing" /* Initializing */);
|
|
30217
|
-
}
|
|
30218
|
-
};
|
|
30219
|
-
var ActivationStageImpl = class {
|
|
30220
|
-
constructor() {
|
|
30221
|
-
this.id = "stage_activation";
|
|
30222
|
-
this.name = "Activating" /* Activating */;
|
|
30223
|
-
this.description = "Plugin Host discovery & ESM plugins activation";
|
|
30224
|
-
}
|
|
30225
|
-
async execute(context) {
|
|
30226
|
-
context.setStage("Activating" /* Activating */);
|
|
30227
|
-
}
|
|
30228
|
-
};
|
|
30229
|
-
var ReadyStageImpl = class {
|
|
30230
|
-
constructor() {
|
|
30231
|
-
this.id = "stage_ready";
|
|
30232
|
-
this.name = "Ready" /* Ready */;
|
|
30233
|
-
this.description = "HTTP & Socket.IO server readiness";
|
|
30234
|
-
}
|
|
30235
|
-
async execute(context) {
|
|
30236
|
-
context.setStage("Ready" /* Ready */);
|
|
30237
|
-
}
|
|
30238
|
-
};
|
|
30239
|
-
|
|
30240
|
-
// packages/core/bootstrap/pipeline/bootstrap-pipeline.ts
|
|
30241
|
-
var BootstrapPipeline = class {
|
|
30242
|
-
constructor(stages) {
|
|
30243
|
-
this._stages = [];
|
|
30244
|
-
this.executor = new PipelineExecutor();
|
|
30245
|
-
if (stages && stages.length > 0) {
|
|
30246
|
-
this._stages = [...stages];
|
|
30247
|
-
} else {
|
|
30248
|
-
this._stages = [
|
|
30249
|
-
new StartupStageImpl(),
|
|
30250
|
-
new RegistrationStageImpl(),
|
|
30251
|
-
new InitializationStageImpl(),
|
|
30252
|
-
new ActivationStageImpl(),
|
|
30253
|
-
new ReadyStageImpl()
|
|
30254
|
-
];
|
|
30255
|
-
}
|
|
30256
|
-
}
|
|
30257
|
-
get stages() {
|
|
30258
|
-
return Object.freeze(this._stages);
|
|
30259
|
-
}
|
|
30260
|
-
addStage(stage) {
|
|
30261
|
-
this._stages.push(stage);
|
|
30262
|
-
return this;
|
|
30263
|
-
}
|
|
30264
|
-
addListener(listener) {
|
|
30265
|
-
return this.executor.addListener(listener);
|
|
30266
|
-
}
|
|
30267
|
-
async run(context) {
|
|
30268
|
-
const result = await this.execute(context);
|
|
30269
|
-
if (result.status === "Failed") {
|
|
30270
|
-
throw result.error || new Error(`Bootstrap Pipeline failed at stage: ${result.failedStage}`);
|
|
30271
|
-
}
|
|
30272
|
-
}
|
|
30273
|
-
async execute(context) {
|
|
30274
|
-
return this.executor.execute(this._stages, context);
|
|
30275
|
-
}
|
|
30276
|
-
};
|
|
30277
|
-
|
|
30278
|
-
// packages/core/bootstrap/builder/platform-builder.ts
|
|
30279
|
-
var DefaultPlatformLogger = class {
|
|
30280
|
-
info(message, ...args) {
|
|
30281
|
-
console.log(`[PlatformBuilder:INFO] ${message}`, ...args);
|
|
30282
|
-
}
|
|
30283
|
-
warn(message, ...args) {
|
|
30284
|
-
console.warn(`[PlatformBuilder:WARN] ${message}`, ...args);
|
|
30285
|
-
}
|
|
30286
|
-
error(message, ...args) {
|
|
30287
|
-
console.error(`[PlatformBuilder:ERROR] ${message}`, ...args);
|
|
30288
|
-
}
|
|
30289
|
-
debug(message, ...args) {
|
|
30290
|
-
console.debug(`[PlatformBuilder:DEBUG] ${message}`, ...args);
|
|
30291
|
-
}
|
|
30292
|
-
};
|
|
30293
|
-
var PlatformBuilder = class _PlatformBuilder {
|
|
30294
|
-
constructor(options) {
|
|
30295
|
-
this._state = "Created";
|
|
30296
|
-
this._config = { ...DEFAULT_BOOTSTRAP_CONFIG };
|
|
30297
|
-
this._logger = new DefaultPlatformLogger();
|
|
30298
|
-
this._environment = "development";
|
|
30299
|
-
this._metadata = /* @__PURE__ */ new Map();
|
|
30300
|
-
this._services = /* @__PURE__ */ new Map();
|
|
30301
|
-
this._capabilities = /* @__PURE__ */ new Map();
|
|
30302
|
-
this._extensions = /* @__PURE__ */ new Map();
|
|
30303
|
-
this._pipeline = new BootstrapPipeline();
|
|
30304
|
-
this._state = "Created";
|
|
30305
|
-
this._logger.info("Builder Started");
|
|
30306
|
-
if (options) {
|
|
30307
|
-
if (options.config) this.withConfiguration(options.config);
|
|
30308
|
-
if (options.logger) this.withLogger(options.logger);
|
|
30309
|
-
if (options.environment) this.withEnvironment(options.environment);
|
|
30310
|
-
if (options.metadata) {
|
|
30311
|
-
for (const [k, v] of Object.entries(options.metadata)) {
|
|
30312
|
-
this.withMetadata(k, v);
|
|
30313
|
-
}
|
|
30314
|
-
}
|
|
30315
|
-
}
|
|
30316
|
-
}
|
|
30317
|
-
static create(options) {
|
|
30318
|
-
return new _PlatformBuilder(options);
|
|
30319
|
-
}
|
|
30320
|
-
get state() {
|
|
30321
|
-
return this._state;
|
|
30322
|
-
}
|
|
30323
|
-
transitionTo(newState) {
|
|
30324
|
-
if (this._state === "Disposed") {
|
|
30325
|
-
throw new Error("PlatformBuilder has been disposed and cannot accept further transitions.");
|
|
30326
|
-
}
|
|
30327
|
-
if (this._state === "Built" && newState !== "Disposed") {
|
|
30328
|
-
throw new Error(`Invalid state transition from Built to ${newState}. Builder is already built.`);
|
|
30329
|
-
}
|
|
30330
|
-
this._state = newState;
|
|
30331
|
-
}
|
|
30332
|
-
withConfiguration(config) {
|
|
30333
|
-
this.transitionTo("Configuring");
|
|
30334
|
-
this._config = { ...this._config, ...config };
|
|
30335
|
-
if (config.environment) {
|
|
30336
|
-
this._environment = config.environment;
|
|
30337
|
-
}
|
|
30338
|
-
this._logger.info("Configuration Loaded");
|
|
30339
|
-
return this;
|
|
30340
|
-
}
|
|
30341
|
-
withLogger(logger2) {
|
|
30342
|
-
this.transitionTo("Configuring");
|
|
30343
|
-
this._logger = logger2;
|
|
30344
|
-
return this;
|
|
30345
|
-
}
|
|
30346
|
-
withEnvironment(env) {
|
|
30347
|
-
this.transitionTo("Configuring");
|
|
30348
|
-
this._environment = env;
|
|
30349
|
-
this._config = { ...this._config, environment: env };
|
|
30350
|
-
return this;
|
|
30351
|
-
}
|
|
30352
|
-
withMetadata(key, value) {
|
|
30353
|
-
this.transitionTo("Configuring");
|
|
30354
|
-
this._metadata.set(key, value);
|
|
30355
|
-
return this;
|
|
30356
|
-
}
|
|
30357
|
-
addBootstrapStage(stage) {
|
|
30358
|
-
this.transitionTo("Configuring");
|
|
30359
|
-
this._pipeline.addStage(stage);
|
|
30360
|
-
return this;
|
|
30361
|
-
}
|
|
30362
|
-
addService(serviceId, instance) {
|
|
30363
|
-
this.transitionTo("Configuring");
|
|
30364
|
-
this._services.set(serviceId, instance);
|
|
30365
|
-
return this;
|
|
30366
|
-
}
|
|
30367
|
-
addCapability(capabilityId, descriptor) {
|
|
30368
|
-
this.transitionTo("Configuring");
|
|
30369
|
-
this._capabilities.set(capabilityId, descriptor);
|
|
30370
|
-
return this;
|
|
30371
|
-
}
|
|
30372
|
-
addExtension(extensionId, extensionSpec) {
|
|
30373
|
-
this.transitionTo("Configuring");
|
|
30374
|
-
this._extensions.set(extensionId, extensionSpec);
|
|
30375
|
-
return this;
|
|
30376
|
-
}
|
|
30377
|
-
buildResult() {
|
|
30378
|
-
const buildStart = Date.now();
|
|
30379
|
-
this.transitionTo("Validating");
|
|
30380
|
-
this._logger.info("Validation Started");
|
|
30381
|
-
const validation = BuilderValidationEngine.validate(this._config, this._environment, this._pipeline.stages);
|
|
30382
|
-
this._logger.info("Validation Completed");
|
|
30383
|
-
if (!validation.isValid) {
|
|
30384
|
-
this._logger.error(`Validation Failed with ${validation.errors.length} errors.`);
|
|
30385
|
-
throw new ConfigurationError(
|
|
30386
|
-
`PlatformBuilder validation failed: ${validation.errors.map((e) => e.message).join("; ")}`
|
|
30387
|
-
);
|
|
30388
|
-
}
|
|
30389
|
-
this.transitionTo("Building");
|
|
30390
|
-
this._logger.info("Build Started");
|
|
30391
|
-
const platformContext = {
|
|
30392
|
-
platformId: `plt_${globalThis.crypto.randomUUID()}`,
|
|
30393
|
-
version: PLATFORM_VERSION2,
|
|
30394
|
-
namespace: "openlearn.core",
|
|
30395
|
-
mode: this._config.mode,
|
|
30396
|
-
environment: {
|
|
30397
|
-
type: this._environment,
|
|
30398
|
-
isDevelopment: this._environment === "development",
|
|
30399
|
-
isProduction: this._environment === "production",
|
|
30400
|
-
isTest: this._environment === "test",
|
|
30401
|
-
isPluginSandbox: false
|
|
30402
|
-
},
|
|
30403
|
-
config: this._config,
|
|
30404
|
-
featureFlags: {
|
|
30405
|
-
isEnabled: () => true,
|
|
30406
|
-
getFlag: (_, def) => def,
|
|
30407
|
-
getAllFlags: () => ({})
|
|
30408
|
-
},
|
|
30409
|
-
runtimeMetadata: {
|
|
30410
|
-
os: process.platform,
|
|
30411
|
-
nodeVersion: process.version,
|
|
30412
|
-
arch: process.arch,
|
|
30413
|
-
processId: process.pid,
|
|
30414
|
-
memoryUsage: process.memoryUsage(),
|
|
30415
|
-
buildVersion: PLATFORM_VERSION2
|
|
30416
|
-
},
|
|
30417
|
-
bootstrapState: "Bootstrapping",
|
|
30418
|
-
logger: this._logger,
|
|
30419
|
-
diagnostics: {
|
|
30420
|
-
getMetrics: () => ({ uptime: 0 }),
|
|
30421
|
-
getSystemStatus: () => ({ isHealthy: true, activeServicesCount: this._services.size, uptimeSeconds: 0 })
|
|
30422
|
-
},
|
|
30423
|
-
serviceLocator: {
|
|
30424
|
-
hasService: (id) => this._services.has(id),
|
|
30425
|
-
getService: (id) => this._services.get(id)
|
|
30426
|
-
},
|
|
30427
|
-
capabilityCatalog: {
|
|
30428
|
-
hasCapability: (id) => this._capabilities.has(id),
|
|
30429
|
-
getCapability: (id) => this._capabilities.get(id)
|
|
30430
|
-
},
|
|
30431
|
-
extensionCatalog: {
|
|
30432
|
-
hasExtension: (id) => this._extensions.has(id),
|
|
30433
|
-
getExtension: (id) => this._extensions.get(id)
|
|
30434
|
-
},
|
|
30435
|
-
eventDispatcher: {
|
|
30436
|
-
dispatch: async () => {
|
|
30437
|
-
}
|
|
30438
|
-
},
|
|
30439
|
-
shutdownToken: {
|
|
30440
|
-
isRequested: false,
|
|
30441
|
-
onShutdown: () => {
|
|
30442
|
-
}
|
|
30443
|
-
}
|
|
30444
|
-
};
|
|
30445
|
-
const buildDurationMs = Date.now() - buildStart;
|
|
30446
|
-
this.transitionTo("Built");
|
|
30447
|
-
this._logger.info(`Build Completed in ${buildDurationMs} ms`);
|
|
30448
|
-
const result = {
|
|
30449
|
-
platformContext,
|
|
30450
|
-
pipeline: this._pipeline,
|
|
30451
|
-
buildDurationMs,
|
|
30452
|
-
validation,
|
|
30453
|
-
metadata: Object.fromEntries(this._metadata.entries()),
|
|
30454
|
-
builderVersion: PLATFORM_VERSION2
|
|
30455
|
-
};
|
|
30456
|
-
return result;
|
|
30457
|
-
}
|
|
30458
|
-
async build() {
|
|
30459
|
-
const res = this.buildResult();
|
|
30460
|
-
return {
|
|
30461
|
-
currentStage: "Created" /* Created */,
|
|
30462
|
-
context: res.platformContext,
|
|
30463
|
-
pipeline: res.pipeline,
|
|
30464
|
-
start: async () => {
|
|
30465
|
-
},
|
|
30466
|
-
shutdown: async () => {
|
|
30467
|
-
},
|
|
30468
|
-
dispose: async () => {
|
|
30469
|
-
}
|
|
30470
|
-
};
|
|
30471
|
-
}
|
|
30472
|
-
async start() {
|
|
30473
|
-
const kernel = await this.build();
|
|
30474
|
-
await kernel.start();
|
|
30475
|
-
return kernel;
|
|
30476
|
-
}
|
|
30477
|
-
async shutdown() {
|
|
30478
|
-
this.transitionTo("Disposed");
|
|
30479
|
-
}
|
|
30480
|
-
dispose() {
|
|
30481
|
-
this._state = "Disposed";
|
|
30482
|
-
this._services.clear();
|
|
30483
|
-
this._capabilities.clear();
|
|
30484
|
-
this._extensions.clear();
|
|
30485
|
-
this._metadata.clear();
|
|
30486
|
-
}
|
|
30487
|
-
};
|
|
30488
|
-
|
|
30489
|
-
// packages/core/bootstrap/composition/platform-composition-root.ts
|
|
30490
|
-
var PlatformCompositionRoot = class _PlatformCompositionRoot {
|
|
30491
|
-
constructor() {
|
|
30492
|
-
this._state = "Created";
|
|
30493
|
-
this._modules = [];
|
|
30494
|
-
this._state = "Created";
|
|
30495
|
-
}
|
|
30496
|
-
static create() {
|
|
30497
|
-
return new _PlatformCompositionRoot();
|
|
30498
|
-
}
|
|
30499
|
-
get state() {
|
|
30500
|
-
return this._state;
|
|
30501
|
-
}
|
|
30502
|
-
registerModule(module2) {
|
|
30503
|
-
if (this._state === "Disposed" || this._state === "Composed") {
|
|
30504
|
-
throw new Error(`Cannot register composition module in state: ${this._state}`);
|
|
30505
|
-
}
|
|
30506
|
-
this._modules.push(module2);
|
|
30507
|
-
return this;
|
|
30508
|
-
}
|
|
30509
|
-
compose(options) {
|
|
30510
|
-
const startTime = Date.now();
|
|
30511
|
-
this._state = "Validating";
|
|
30512
|
-
const opts = options || {};
|
|
30513
|
-
const builder = PlatformBuilder.create();
|
|
30514
|
-
if (opts.config) builder.withConfiguration(opts.config);
|
|
30515
|
-
if (opts.logger) builder.withLogger(opts.logger);
|
|
30516
|
-
if (opts.environment) builder.withEnvironment(opts.environment);
|
|
30517
|
-
const builderResult = builder.buildResult();
|
|
30518
|
-
const validation = CompositionValidator.validate(opts, builderResult.pipeline, this._modules);
|
|
30519
|
-
if (!validation.isValid) {
|
|
30520
|
-
throw new ConfigurationError(
|
|
30521
|
-
`CompositionRoot validation failed: ${validation.errors.map((e) => e.message).join("; ")}`
|
|
30522
|
-
);
|
|
30523
|
-
}
|
|
30524
|
-
this._state = "Composing";
|
|
30525
|
-
for (const mod of this._modules) {
|
|
30526
|
-
mod.compose(opts);
|
|
30527
|
-
}
|
|
30528
|
-
const durationMs = Date.now() - startTime;
|
|
30529
|
-
this._state = "Composed";
|
|
30530
|
-
return {
|
|
30531
|
-
context: builderResult.platformContext,
|
|
30532
|
-
pipeline: builderResult.pipeline,
|
|
30533
|
-
durationMs,
|
|
30534
|
-
validation
|
|
30535
|
-
};
|
|
30536
|
-
}
|
|
30537
|
-
dispose() {
|
|
30538
|
-
this._state = "Disposed";
|
|
30539
|
-
this._modules = [];
|
|
30540
|
-
}
|
|
30541
|
-
};
|
|
30542
|
-
|
|
30543
|
-
// packages/core/bootstrap/permission/permission-registry.ts
|
|
30544
|
-
var PermissionRegistry = class {
|
|
30545
|
-
constructor() {
|
|
30546
|
-
this.descriptors = /* @__PURE__ */ new Map();
|
|
30547
|
-
}
|
|
30548
|
-
register(descriptor) {
|
|
30549
|
-
if (!descriptor || !descriptor.id) {
|
|
30550
|
-
throw new Error("PermissionRegistry Error: PermissionDescriptor must have a valid ID.");
|
|
30551
|
-
}
|
|
30552
|
-
if (this.descriptors.has(descriptor.id)) {
|
|
30553
|
-
throw new Error(`PermissionRegistry Collision: Permission '${descriptor.id}' is already registered.`);
|
|
30554
|
-
}
|
|
30555
|
-
this.descriptors.set(descriptor.id, Object.freeze({ ...descriptor }));
|
|
30556
|
-
}
|
|
30557
|
-
unregister(permissionId) {
|
|
30558
|
-
return this.descriptors.delete(permissionId);
|
|
30559
|
-
}
|
|
30560
|
-
get(permissionId) {
|
|
30561
|
-
return this.descriptors.get(permissionId);
|
|
30562
|
-
}
|
|
30563
|
-
exists(permissionId) {
|
|
30564
|
-
return this.descriptors.has(permissionId);
|
|
30565
|
-
}
|
|
30566
|
-
list() {
|
|
30567
|
-
return Object.freeze(Array.from(this.descriptors.values()));
|
|
30568
|
-
}
|
|
30569
|
-
clear() {
|
|
30570
|
-
this.descriptors.clear();
|
|
30571
|
-
}
|
|
30572
|
-
};
|
|
30573
|
-
|
|
30574
|
-
// packages/core/bootstrap/permission/permission-evaluator.ts
|
|
30575
|
-
var PermissionEvaluator = class {
|
|
30576
|
-
static async evaluate(subject, permission, explicitGrants, descriptor, providers) {
|
|
30577
|
-
const evaluatedAt = Date.now();
|
|
30578
|
-
const grantKey = `${subject}::${permission}`;
|
|
30579
|
-
if (explicitGrants.has(grantKey)) {
|
|
30580
|
-
const policy = explicitGrants.get(grantKey);
|
|
30581
|
-
if (policy === "Allow") {
|
|
30582
|
-
return { allowed: true, policy: "Allow", reason: "Explicitly granted to subject", evaluatedAt };
|
|
30583
|
-
}
|
|
30584
|
-
if (policy === "Deny") {
|
|
30585
|
-
return { allowed: false, policy: "Deny", reason: "Explicitly denied to subject", evaluatedAt };
|
|
30586
|
-
}
|
|
30587
|
-
}
|
|
30588
|
-
if (providers && providers.length > 0) {
|
|
30589
|
-
for (const provider of providers) {
|
|
30590
|
-
const policy = await provider.getPolicy(subject, permission);
|
|
30591
|
-
if (policy === "Allow") {
|
|
30592
|
-
return { allowed: true, policy: "Allow", reason: `Granted by provider '${provider.id}'`, evaluatedAt };
|
|
30593
|
-
}
|
|
30594
|
-
if (policy === "Deny") {
|
|
30595
|
-
return { allowed: false, policy: "Deny", reason: `Denied by provider '${provider.id}'`, evaluatedAt };
|
|
30596
|
-
}
|
|
30597
|
-
}
|
|
30598
|
-
}
|
|
30599
|
-
if (descriptor && descriptor.defaultPolicy) {
|
|
30600
|
-
const defaultPol = descriptor.defaultPolicy;
|
|
30601
|
-
if (defaultPol === "Allow") {
|
|
30602
|
-
return { allowed: true, policy: "Allow", reason: "Allowed by descriptor default policy", evaluatedAt };
|
|
30603
|
-
}
|
|
30604
|
-
if (defaultPol === "Deny") {
|
|
30605
|
-
return { allowed: false, policy: "Deny", reason: "Denied by descriptor default policy", evaluatedAt };
|
|
30606
|
-
}
|
|
30607
|
-
}
|
|
30608
|
-
return {
|
|
30609
|
-
allowed: false,
|
|
30610
|
-
policy: "Default",
|
|
30611
|
-
reason: "No explicit allow policy resolved; default deny",
|
|
30612
|
-
evaluatedAt
|
|
30613
|
-
};
|
|
30614
|
-
}
|
|
30615
|
-
};
|
|
30616
|
-
|
|
30617
|
-
// packages/core/bootstrap/permission/permission-manager.ts
|
|
30618
|
-
var PermissionManager = class {
|
|
30619
|
-
constructor() {
|
|
30620
|
-
this.registry = new PermissionRegistry();
|
|
30621
|
-
this.grants = /* @__PURE__ */ new Map();
|
|
30622
|
-
this.providers = [];
|
|
30623
|
-
}
|
|
30624
|
-
register(descriptor) {
|
|
30625
|
-
this.registry.register(descriptor);
|
|
30626
|
-
}
|
|
30627
|
-
unregister(permissionId) {
|
|
30628
|
-
return this.registry.unregister(permissionId);
|
|
30629
|
-
}
|
|
30630
|
-
grant(subject, permissionId, policy = "Allow") {
|
|
30631
|
-
const key = `${subject}::${permissionId}`;
|
|
30632
|
-
this.grants.set(key, policy);
|
|
30633
|
-
}
|
|
30634
|
-
revoke(subject, permissionId) {
|
|
30635
|
-
const key = `${subject}::${permissionId}`;
|
|
30636
|
-
this.grants.delete(key);
|
|
30637
|
-
}
|
|
30638
|
-
addProvider(provider) {
|
|
30639
|
-
if (!this.providers.some((p) => p.id === provider.id)) {
|
|
30640
|
-
this.providers.push(provider);
|
|
30641
|
-
}
|
|
30642
|
-
}
|
|
30643
|
-
removeProvider(providerId) {
|
|
30644
|
-
this.providers = this.providers.filter((p) => p.id !== providerId);
|
|
30645
|
-
}
|
|
30646
|
-
exists(permissionId) {
|
|
30647
|
-
return this.registry.exists(permissionId);
|
|
30648
|
-
}
|
|
30649
|
-
list() {
|
|
30650
|
-
return this.registry.list();
|
|
30651
|
-
}
|
|
30652
|
-
async check(subject, target, permissionId, metadata) {
|
|
30653
|
-
const timestamp = Date.now();
|
|
30654
|
-
const descriptor = this.registry.get(permissionId);
|
|
30655
|
-
const result = await PermissionEvaluator.evaluate(subject, permissionId, this.grants, descriptor, this.providers);
|
|
30656
|
-
const context = {
|
|
30657
|
-
subject,
|
|
30658
|
-
target,
|
|
30659
|
-
permission: permissionId,
|
|
30660
|
-
metadata,
|
|
30661
|
-
timestamp,
|
|
30662
|
-
result
|
|
30663
|
-
};
|
|
30664
|
-
return context;
|
|
30665
|
-
}
|
|
30666
|
-
async require(subject, target, permissionId, metadata) {
|
|
30667
|
-
const context = await this.check(subject, target, permissionId, metadata);
|
|
30668
|
-
if (!context.result || !context.result.allowed) {
|
|
30669
|
-
const reason = context.result?.reason || "Permission Denied";
|
|
30670
|
-
throw new Error(
|
|
30671
|
-
`Infrastructure Permission Exception: Subject '${subject}' is denied '${permissionId}' on target '${target}'. Reason: ${reason}`
|
|
30672
|
-
);
|
|
30673
|
-
}
|
|
30674
|
-
return context;
|
|
30675
|
-
}
|
|
30676
|
-
clear() {
|
|
30677
|
-
this.registry.clear();
|
|
30678
|
-
this.grants.clear();
|
|
30679
|
-
this.providers = [];
|
|
30680
|
-
}
|
|
30681
|
-
};
|
|
30682
|
-
|
|
30683
|
-
// packages/core/bootstrap/composition/plugin-composition-module.ts
|
|
30684
|
-
var _sharedPlatformRegistry;
|
|
30685
|
-
function getPlatformServiceRegistry() {
|
|
30686
|
-
if (!_sharedPlatformRegistry) {
|
|
30687
|
-
_sharedPlatformRegistry = new PlatformServiceRegistry();
|
|
30688
|
-
}
|
|
30689
|
-
return _sharedPlatformRegistry;
|
|
30690
|
-
}
|
|
30691
|
-
var PluginCompositionModule = class {
|
|
30692
|
-
constructor() {
|
|
30693
|
-
this.id = "mod_plugin_composition";
|
|
30694
|
-
this.name = "PluginCompositionModule";
|
|
30695
|
-
}
|
|
30696
|
-
compose(options) {
|
|
30697
|
-
const serviceRegistry = getPlatformServiceRegistry();
|
|
30698
|
-
const registerOrReplace = (config) => {
|
|
30699
|
-
const { id, instance } = config;
|
|
30700
|
-
if (serviceRegistry.has(id)) {
|
|
30701
|
-
serviceRegistry.replace(id, instance);
|
|
30702
|
-
} else {
|
|
30703
|
-
serviceRegistry.register({ id }, instance);
|
|
30704
|
-
}
|
|
30705
|
-
};
|
|
30706
|
-
const capabilityRegistry = new CapabilityRegistry();
|
|
30707
|
-
const permissionManager = new PermissionManager();
|
|
30708
|
-
const eventBus = new EventBus();
|
|
30709
|
-
const realPluginHost = options?.infrastructureRefs?.get("pluginHost") ?? {
|
|
30710
|
-
name: "PluginHostService",
|
|
30711
|
-
isReady: true,
|
|
30712
|
-
pluginsCount: 0
|
|
30713
|
-
};
|
|
30714
|
-
const realContributionRegistry = options?.infrastructureRefs?.get("contributionRegistry") ?? {
|
|
30715
|
-
name: "ContributionRegistry",
|
|
30716
|
-
slotsCount: 0
|
|
30717
|
-
};
|
|
30718
|
-
const realRuntimeComposition = options?.infrastructureRefs?.get("runtimeComposition") ?? {
|
|
30719
|
-
name: "PluginRuntimeComposition",
|
|
30720
|
-
isStarted: true
|
|
30721
|
-
};
|
|
30722
|
-
const realLifecycleManager = options?.infrastructureRefs?.get("lifecycleManager") ?? {
|
|
30723
|
-
name: "PluginLifecycleManager",
|
|
30724
|
-
version: PLATFORM_VERSION
|
|
30725
|
-
};
|
|
30726
|
-
const realCapabilityGateway = options?.infrastructureRefs?.get("capabilityGateway") ?? {
|
|
30727
|
-
name: "PluginCapabilityGateway",
|
|
30728
|
-
version: PLATFORM_VERSION
|
|
30729
|
-
};
|
|
30730
|
-
const realExtensionRegistry = options?.infrastructureRefs?.get("extensionRegistry") ?? {
|
|
30731
|
-
name: "UnifiedExtensionRegistry",
|
|
30732
|
-
version: PLATFORM_VERSION
|
|
30733
|
-
};
|
|
30734
|
-
const realDistributionManager = options?.infrastructureRefs?.get("distributionManager") ?? {
|
|
30735
|
-
name: "PluginDistributionManager",
|
|
30736
|
-
version: PLATFORM_VERSION
|
|
30737
|
-
};
|
|
30738
|
-
registerOrReplace({
|
|
30739
|
-
id: "srv_plugin_host",
|
|
30740
|
-
lifetime: "Singleton",
|
|
30741
|
-
description: "OpenLearn Plugin Host Service Engine",
|
|
30742
|
-
instance: realPluginHost
|
|
30743
|
-
});
|
|
30744
|
-
registerOrReplace({
|
|
30745
|
-
id: "srv_plugin_contribution_registry",
|
|
30746
|
-
lifetime: "Singleton",
|
|
30747
|
-
description: "Plugin UI & Slot Contribution Registry",
|
|
30748
|
-
instance: realContributionRegistry
|
|
30749
|
-
});
|
|
30750
|
-
registerOrReplace({
|
|
30751
|
-
id: "srv_plugin_runtime_composition",
|
|
30752
|
-
lifetime: "Singleton",
|
|
30753
|
-
description: "OpenLearn Plugin Runtime Composition Facade",
|
|
30754
|
-
instance: realRuntimeComposition
|
|
30755
|
-
});
|
|
30756
|
-
registerOrReplace({
|
|
30757
|
-
id: "srv_plugin_lifecycle_manager",
|
|
30758
|
-
lifetime: "Singleton",
|
|
30759
|
-
description: "OpenLearn Unified Plugin Lifecycle Manager",
|
|
30760
|
-
instance: realLifecycleManager
|
|
30761
|
-
});
|
|
30762
|
-
registerOrReplace({
|
|
30763
|
-
id: "srv_plugin_capability_gateway",
|
|
30764
|
-
lifetime: "Singleton",
|
|
30765
|
-
description: "OpenLearn Unified Plugin Capability Gateway",
|
|
30766
|
-
instance: realCapabilityGateway
|
|
30767
|
-
});
|
|
30768
|
-
registerOrReplace({
|
|
30769
|
-
id: "srv_unified_extension_registry",
|
|
30770
|
-
lifetime: "Singleton",
|
|
30771
|
-
description: "OpenLearn Unified Extension Registry Foundation",
|
|
30772
|
-
instance: realExtensionRegistry
|
|
30773
|
-
});
|
|
30774
|
-
registerOrReplace({
|
|
30775
|
-
id: "srv_plugin_distribution_manager",
|
|
30776
|
-
lifetime: "Singleton",
|
|
30777
|
-
description: "OpenLearn Plugin Distribution Manager Foundation",
|
|
30778
|
-
instance: realDistributionManager
|
|
30779
|
-
});
|
|
30780
|
-
permissionManager.register({
|
|
30781
|
-
id: "perm_plugin_execute",
|
|
30782
|
-
name: "Plugin Execution Permission",
|
|
30783
|
-
category: "Infrastructure",
|
|
30784
|
-
description: "Allows plugin sandbox execution within Platform",
|
|
30785
|
-
defaultPolicy: "Allow"
|
|
30786
|
-
});
|
|
30787
|
-
permissionManager.register({
|
|
30788
|
-
id: "perm_plugin_install",
|
|
30789
|
-
name: "Plugin Installation Permission",
|
|
30790
|
-
category: "Infrastructure",
|
|
30791
|
-
description: "Allows installing new plugin packages into storage",
|
|
30792
|
-
defaultPolicy: "Allow"
|
|
30793
|
-
});
|
|
30794
|
-
eventBus.publish({
|
|
30795
|
-
id: `evt_${globalThis.crypto.randomUUID()}`,
|
|
30796
|
-
type: "PluginHostInitialized",
|
|
30797
|
-
source: "PluginCompositionModule",
|
|
30798
|
-
payload: { timestamp: Date.now(), status: "Ready" },
|
|
30799
|
-
timestamp: Date.now()
|
|
30800
|
-
});
|
|
30801
|
-
eventBus.publish({
|
|
30802
|
-
id: `evt_${globalThis.crypto.randomUUID()}`,
|
|
30803
|
-
type: "PluginLoaded",
|
|
30804
|
-
source: "PluginCompositionModule",
|
|
30805
|
-
payload: { timestamp: Date.now(), pluginsLoaded: 0 },
|
|
30806
|
-
timestamp: Date.now()
|
|
30807
|
-
});
|
|
30808
|
-
eventBus.publish({
|
|
30809
|
-
id: `evt_${globalThis.crypto.randomUUID()}`,
|
|
30810
|
-
type: "PluginActivated",
|
|
30811
|
-
source: "PluginCompositionModule",
|
|
30812
|
-
payload: { timestamp: Date.now(), activePlugins: [] },
|
|
30813
|
-
timestamp: Date.now()
|
|
30814
|
-
});
|
|
30815
|
-
}
|
|
30816
|
-
};
|
|
30817
|
-
|
|
30818
|
-
// packages/core/kernel/index.ts
|
|
30819
|
-
var import_path12 = __toESM(require("path"), 1);
|
|
30820
|
-
var Kernel = class {
|
|
30821
|
-
/**
|
|
30822
|
-
* @param opts.pluginsDir - 插件产物目录。生产默认 `<cwd>/plugins`;
|
|
30823
|
-
* 测试**必须**显式传入临时目录,否则插件产物会落进工作树。
|
|
30824
|
-
*
|
|
30825
|
-
* ## 为什么要开这个口子(H-1)
|
|
30826
|
-
*
|
|
30827
|
-
* 原本这里硬编码 `path.resolve(process.cwd(), 'plugins')` 且不可覆盖。
|
|
30828
|
-
* 后果不只是「测试会弄脏工作树」,更根本的是 **Kernel 在这个维度上不可测** ——
|
|
30829
|
-
* 没有任何办法在测试里把插件产物引到别处,于是污染一直无人察觉:
|
|
30830
|
-
* 实测仓库 `plugins/` 下积累了 **1705 个孤儿目录 / 24MB**,且每次 `pnpm test` 继续增长。
|
|
30831
|
-
* 该目录被 `.gitignore` 忽略,所以 `git status` 完全看不见。
|
|
30832
|
-
*
|
|
30833
|
-
* 加可选参数后,生产行为**逐字节不变**(默认值相同),而测试可把产物引到
|
|
30834
|
-
* `os.tmpdir()`。`vitest.setup.ts` 的全局守卫会把「仍写工作树」变成硬失败,
|
|
30835
|
-
* 防止将来有人再引入同类调用。
|
|
30836
|
-
*/
|
|
30837
|
-
constructor(opts) {
|
|
30838
|
-
this.db = db;
|
|
30839
|
-
this.readPool = readPool;
|
|
30840
|
-
this.getReadDb = getReadDb;
|
|
30841
|
-
this.queryRead = queryRead;
|
|
30842
|
-
this.queryReadOne = queryReadOne;
|
|
30843
|
-
this.checkpoint = checkpoint;
|
|
30844
|
-
this.eventBus = new EventBus();
|
|
30845
|
-
this.capabilityGuard = new CapabilityGuard();
|
|
30846
|
-
this.serviceRegistry = new ServiceRegistry();
|
|
30847
|
-
this.storageService = new StorageService(this.db);
|
|
30848
|
-
this.aiService = new AIService(this.db);
|
|
30849
|
-
this.aiRuntime = new AIRuntimeKernel();
|
|
30850
|
-
this.aiCapability = new AICapabilityKernel(this.aiRuntime);
|
|
30851
|
-
this.capabilityFrameworkRuntime = new CapabilityRuntimeKernel(this.aiCapability);
|
|
30852
|
-
this.capabilityGovernance = new CapabilityGovernanceKernel();
|
|
30853
|
-
this.platformServiceRegistryKernel = new ServiceRegistryKernel();
|
|
30854
|
-
this.analyticsEngine = new AnalyticsEngineKernel();
|
|
30855
|
-
this.collaborationEngine = new CollaborationEngineKernel();
|
|
30856
|
-
this.presenceEngine = new PresenceEngineKernel();
|
|
30857
|
-
this.classroomRuntime = new ClassroomRuntimeKernel();
|
|
30858
|
-
this.lessonRuntime = new LessonRuntime({ eventBus: this.eventBus });
|
|
30859
|
-
this.lessonRuntime.aiInterface.setAIService(this.aiService);
|
|
30860
|
-
this.commandBus = new CommandBus(this.eventBus);
|
|
30861
|
-
this.actionRegistry = new ActionRegistry();
|
|
30862
|
-
this.processManager = new ProcessManager(this);
|
|
30863
|
-
this.esmLoader = new NodeEsmLoader();
|
|
30864
|
-
const pluginsDir = opts?.pluginsDir ?? import_path12.default.resolve(process.cwd(), "plugins");
|
|
30865
|
-
try {
|
|
30866
|
-
import_fs13.default.mkdirSync(pluginsDir, { recursive: true });
|
|
30867
|
-
} catch (err) {
|
|
30868
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
30869
|
-
throw new Error(
|
|
30870
|
-
`[Kernel] FATAL: \u65E0\u6CD5\u521B\u5EFA\u63D2\u4EF6\u76EE\u5F55 ${pluginsDir}\uFF08${detail}\uFF09\u3002\u5BB9\u5668\u8BF7\u628A OPENLEARN_DATA_DIR \u6216 OPENLEARN_PLUGINS_DIR \u6307\u5230\u53EF\u5199\u5377\uFF0C\u4F8B\u5982 /data/plugins\u3002`
|
|
30871
|
-
);
|
|
30872
|
-
}
|
|
30873
|
-
this.pluginHost = new PluginHost(this.serviceRegistry, this.esmLoader, this.db, pluginsDir);
|
|
30874
|
-
setPluginNamespaceResolver((namespace) => this.pluginHost.findByManifestId(namespace) ? namespace : void 0);
|
|
30875
|
-
this.workerManager = new WorkerManager(this.serviceRegistry, this.capabilityGuard, this.db);
|
|
30876
|
-
this.pluginHost.setWorkerManager(this.workerManager);
|
|
30877
|
-
const capabilityRegistry = this.aiCapability.registry;
|
|
30878
|
-
this.pluginRuntimeComposition = new PluginRuntimeComposition(this.pluginHost, this.workerManager);
|
|
30879
|
-
this.pluginLifecycleManager = new PluginLifecycleManager(this.pluginHost);
|
|
30880
|
-
this.pluginDistributionManager = new PluginDistributionManager(this.pluginHost);
|
|
30881
|
-
this.unifiedExtensionRegistry = new UnifiedExtensionRegistry();
|
|
30882
|
-
this.capabilityRegistry = capabilityRegistry;
|
|
30883
|
-
this.pluginCapabilityGateway = new PluginCapabilityGateway(capabilityRegistry);
|
|
30884
|
-
const infrastructureRefs = /* @__PURE__ */ new Map([
|
|
30885
|
-
["pluginHost", this.pluginHost],
|
|
30886
|
-
["contributionRegistry", this.pluginHost.getContributionRegistry()],
|
|
30887
|
-
["runtimeComposition", this.pluginRuntimeComposition],
|
|
30888
|
-
["lifecycleManager", this.pluginLifecycleManager],
|
|
30889
|
-
["capabilityGateway", this.pluginCapabilityGateway],
|
|
30890
|
-
["extensionRegistry", this.unifiedExtensionRegistry],
|
|
30891
|
-
["distributionManager", this.pluginDistributionManager]
|
|
30892
|
-
]);
|
|
30893
|
-
try {
|
|
30894
|
-
PlatformCompositionRoot.create().registerModule(new PluginCompositionModule()).compose({ infrastructureRefs });
|
|
30895
|
-
} catch (e) {
|
|
30896
|
-
console.warn("[P7-A2] Plugin composition failed (non-fatal):", e.message);
|
|
30897
|
-
}
|
|
30898
|
-
this.serviceRegistry.register(IEventBusServiceToken3, this.eventBus);
|
|
30899
|
-
this.serviceRegistry.register(ICapabilityServiceToken, this.capabilityGuard);
|
|
30900
|
-
this.serviceRegistry.register(IStorageServiceToken, this.storageService);
|
|
30901
|
-
this.serviceRegistry.register(ICommandBusServiceToken4, this.commandBus);
|
|
30902
|
-
this.serviceRegistry.register(IActionRegistryServiceToken4, this.actionRegistry);
|
|
30903
|
-
this.serviceRegistry.register(IProcessServiceToken2, this.processManager);
|
|
30904
|
-
this.serviceRegistry.register(IAIServiceToken, this.aiService);
|
|
30905
|
-
this.serviceRegistry.register(IDatabaseToken4, this.db);
|
|
30906
|
-
this.serviceRegistry.register(IPluginHostToken, this.pluginHost);
|
|
30907
|
-
this.serviceRegistry.register(IPluginLifecycleManagerToken, this.pluginLifecycleManager);
|
|
30908
|
-
this.serviceRegistry.register(IPluginDistributionManagerToken, this.pluginDistributionManager);
|
|
30909
|
-
this.serviceRegistry.register(IPluginRuntimeCompositionToken, this.pluginRuntimeComposition);
|
|
30910
|
-
this.serviceRegistry.register(IUnifiedExtensionRegistryToken, this.unifiedExtensionRegistry);
|
|
30911
|
-
this.serviceRegistry.register(IPluginCapabilityGatewayToken, this.pluginCapabilityGateway);
|
|
30912
|
-
this.serviceRegistry.register(ICapabilityRegistryToken, this.capabilityRegistry);
|
|
30913
|
-
this.serviceRegistry.register(ISemesterGradeServiceToken, new SemesterGradeService(this.db));
|
|
30914
|
-
this.serviceRegistry.register(IPointsDimensionRegistryToken, new PointsDimensionRegistry());
|
|
30915
|
-
this.serviceRegistry.register(IPointsLedgerServiceToken, new PointsLedgerService(this.db));
|
|
30916
|
-
this.serviceRegistry.register(IClassRosterServiceToken, new ClassRosterService(this.db));
|
|
30917
|
-
this.serviceRegistry.register(ILessonEngineServiceToken, { getRuntime: async () => this.lessonRuntime });
|
|
30918
|
-
this.serviceRegistry.register(IClassroomRuntimeServiceToken, {
|
|
30919
|
-
getRuntimeKernel: async () => this.classroomRuntime
|
|
30920
|
-
});
|
|
30921
|
-
this.serviceRegistry.register(IPresenceEngineServiceToken, {
|
|
30922
|
-
getPresenceEngine: async () => this.presenceEngine
|
|
30923
|
-
});
|
|
30924
|
-
this.serviceRegistry.register(ITeachingCollaborationServiceToken, {
|
|
30925
|
-
getCollaborationEngine: async () => this.collaborationEngine
|
|
30926
|
-
});
|
|
30927
|
-
this.serviceRegistry.register(ILearningAnalyticsServiceToken, {
|
|
30928
|
-
getAnalyticsEngine: async () => this.analyticsEngine
|
|
30929
|
-
});
|
|
30930
|
-
this.serviceRegistry.register(IAICapabilityServiceToken, {
|
|
30931
|
-
getCapabilityKernel: async () => this.aiCapability
|
|
30932
|
-
});
|
|
30933
|
-
this.serviceRegistry.register(ICapabilityRuntimeServiceToken, {
|
|
30934
|
-
getRuntimeKernel: async () => this.capabilityFrameworkRuntime
|
|
30935
|
-
});
|
|
30936
|
-
this.serviceRegistry.register(ICapabilityGovernanceServiceToken, {
|
|
30937
|
-
getGovernanceKernel: async () => this.capabilityGovernance
|
|
30938
|
-
});
|
|
30939
|
-
this.serviceRegistry.register(IPlatformServiceRegistryToken, {
|
|
30940
|
-
getServiceRegistryKernel: async () => this.platformServiceRegistryKernel
|
|
30941
|
-
});
|
|
30942
|
-
this.serviceRegistry.register(IAuthSessionBridgeToken, new AuthSessionBridgeService(this.db));
|
|
30943
|
-
this.serviceRegistry.register(ICoursewareRuntimeScriptRegistryToken, new CoursewareRuntimeScriptRegistry());
|
|
30944
|
-
this.serviceRegistry.register(IStageGuardServiceToken, defaultStageGuardPipeline);
|
|
30945
|
-
this.serviceRegistry.register(IAssessmentGraderRegistryToken, assessmentGraderRegistry);
|
|
30946
|
-
this.commandBus.setInterceptor(async (command) => {
|
|
30947
|
-
const action = this.actionRegistry.getActionByCommandType(command.type);
|
|
30948
|
-
if (action) {
|
|
30949
|
-
if (action.inputSchema) {
|
|
30950
|
-
const errors = validateJsonSchema(command.payload, action.inputSchema);
|
|
30951
|
-
if (errors.length > 0) {
|
|
30952
|
-
throw new Error(
|
|
30953
|
-
`[PayloadValidationError] Invalid command payload for ${command.type}: ${errors.join("; ")}`
|
|
30954
|
-
);
|
|
30955
|
-
}
|
|
30956
|
-
}
|
|
30957
|
-
const isAdmin = command.actorId === "role:administrator" || command.actorId === "admin" || command.actorId === "usr_admin" || command.actorId === "admin-demo" || typeof command.actorId === "string" && command.actorId.startsWith("user:") && (command.actorId.endsWith(":administrator") || command.actorId.endsWith(":admin"));
|
|
30958
|
-
if (action.capabilityRequired && !isAdmin) {
|
|
30959
|
-
const allowed = this.capabilityGuard.check(command.actorId, action.capabilityRequired);
|
|
30960
|
-
if (!allowed) {
|
|
30961
|
-
const err = new Error(
|
|
30962
|
-
`[CapabilityGuard] Access Denied: Actor ${command.actorId} missing capability ${action.capabilityRequired} for ${command.type}`
|
|
30963
|
-
);
|
|
30964
|
-
err.status = 403;
|
|
30965
|
-
err.code = "COMMAND_CAPABILITY_DENIED";
|
|
30966
|
-
throw err;
|
|
30967
|
-
}
|
|
30968
|
-
}
|
|
30969
|
-
const pluginSuppliedApproval = typeof command.actorId === "string" && command.actorId.startsWith("plugin:");
|
|
30970
|
-
if (action.isHighRisk && (command.metadata?.approved !== true || pluginSuppliedApproval)) {
|
|
30971
|
-
if (isAdmin && !pluginSuppliedApproval) {
|
|
30972
|
-
console.log(
|
|
30973
|
-
`[Security] Command ${command.type} initiated by Administrator (${command.actorId}). Bypassing human approval.`
|
|
30974
|
-
);
|
|
30975
|
-
} else {
|
|
30976
|
-
const stmt = this.db.prepare(
|
|
30977
|
-
"INSERT INTO pending_commands (id, command_type, payload, actor_id, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
30978
|
-
);
|
|
30979
|
-
stmt.run(command.id, command.type, JSON.stringify(command.payload), command.actorId, Date.now());
|
|
30980
|
-
this.eventBus.publish({
|
|
30981
|
-
id: (0, import_uuid15.v7)(),
|
|
30982
|
-
type: "approval.requested",
|
|
30983
|
-
source: "kernel.security",
|
|
30984
|
-
payload: { commandId: command.id, commandType: command.type },
|
|
30985
|
-
timestamp: Date.now(),
|
|
30986
|
-
correlationId: command.id
|
|
30987
|
-
});
|
|
30988
|
-
throw new Error(
|
|
30989
|
-
`[Security] Command ${command.type} requires human approval. It has been queued to pending actions.`
|
|
30990
|
-
);
|
|
30991
|
-
}
|
|
30992
|
-
}
|
|
30993
|
-
} else {
|
|
30994
|
-
const err = new Error(
|
|
30995
|
-
`[Security] Command "${command.type}" has no registered action descriptor and is denied by default. Only commands registered via actionRegistry.register() may be dispatched. If you own this command, register an ActionDescriptor (capabilityRequired + inputSchema).`
|
|
30996
|
-
);
|
|
30997
|
-
err.status = 403;
|
|
30998
|
-
err.code = "COMMAND_NOT_REGISTERED";
|
|
30999
|
-
throw err;
|
|
31000
|
-
}
|
|
31001
|
-
});
|
|
31002
|
-
Promise.resolve().then(() => (init_context_builder(), context_builder_exports)).then((m) => m.bootstrapSharedModules()).catch((err) => {
|
|
31003
|
-
console.warn("[Kernel] Failed to bootstrap shared modules:", err.message);
|
|
31004
|
-
});
|
|
31005
|
-
this.ready = this.bootstrapSystemPlugins().catch((err) => {
|
|
31006
|
-
console.error("[Kernel] Critical system plugin bootstrap failed:", err);
|
|
31007
|
-
process.exit(1);
|
|
31008
|
-
});
|
|
31009
|
-
if (process.env.NODE_ENV === "development") {
|
|
31010
|
-
const watchDir = this.pluginHost.getPluginsDir();
|
|
31011
|
-
try {
|
|
31012
|
-
const hotReload = new HotReloadController(this.pluginHost, watchDir);
|
|
31013
|
-
this.pluginHost.setHotReloadController(hotReload);
|
|
31014
|
-
hotReload.start().catch((err) => {
|
|
31015
|
-
console.warn("[Kernel] Hot reload initialization failed:", err.message);
|
|
31016
|
-
});
|
|
31017
|
-
} catch (err) {
|
|
31018
|
-
console.warn("[Kernel] Hot reload initialization failed:", err.message);
|
|
31019
|
-
}
|
|
31020
|
-
}
|
|
31021
|
-
}
|
|
31022
|
-
async bootstrapSystemPlugins() {
|
|
31023
|
-
await this.migratePluginsToFilesystem();
|
|
31024
|
-
const systemPlugins = [
|
|
31025
|
-
{ id: "@openlearn/plugin-vfs", mod: VfsPlugin, name: "Virtual File System Plugin", critical: true },
|
|
31026
|
-
{ id: "@openlearn/plugin-process", mod: ProcessPlugin, name: "Background Process Plugin", critical: true },
|
|
31027
|
-
{ id: "@openlearn/plugin-management", mod: ManagementPlugin, name: "LMS Management Plugin", critical: true },
|
|
31028
|
-
{ id: "@openlearn/plugin-builtin", mod: BuiltinPlugin, name: "Classroom Builtin Plugin", critical: true },
|
|
31029
|
-
{ id: "@openlearn/plugin-ai-planner", mod: AiPlannerPlugin, name: "AI Planner Plugin", critical: false },
|
|
31030
|
-
{
|
|
31031
|
-
id: "@openlearn/plugin-ai-submit-injector",
|
|
31032
|
-
mod: AiSubmitInjectorPlugin,
|
|
31033
|
-
name: "AI Submit Injector Plugin",
|
|
31034
|
-
critical: false
|
|
31035
|
-
},
|
|
31036
|
-
{
|
|
31037
|
-
id: "@openlearn/plugin-assignment-eval",
|
|
31038
|
-
mod: AssignmentEvalPlugin,
|
|
31039
|
-
name: "Assignment Evaluation and Peer Review Plugin",
|
|
31040
|
-
critical: false
|
|
31041
|
-
}
|
|
31042
|
-
];
|
|
31043
|
-
for (const plugin of systemPlugins) {
|
|
31044
|
-
try {
|
|
31045
|
-
let row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(plugin.id);
|
|
31046
|
-
if (!row) {
|
|
31047
|
-
this.db.prepare(
|
|
31048
|
-
"INSERT INTO plugins (id, name, manifest, source_code, file_path, status, created_at, loader_version, execution_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
31049
|
-
).run(
|
|
31050
|
-
plugin.id,
|
|
31051
|
-
plugin.name,
|
|
31052
|
-
JSON.stringify(plugin.mod.manifest),
|
|
31053
|
-
"",
|
|
31054
|
-
// source_code: system plugins are preloaded in-memory
|
|
31055
|
-
null,
|
|
31056
|
-
// file_path: system plugins have no file
|
|
31057
|
-
"installed",
|
|
31058
|
-
Date.now(),
|
|
31059
|
-
"esm",
|
|
31060
|
-
"inline"
|
|
31061
|
-
);
|
|
31062
|
-
}
|
|
31063
|
-
this.pluginHost.registerPreloadedPlugin(
|
|
31064
|
-
plugin.id,
|
|
31065
|
-
plugin.mod
|
|
31066
|
-
);
|
|
31067
|
-
await this.pluginHost.activatePlugin(plugin.id);
|
|
31068
|
-
} catch (err) {
|
|
31069
|
-
if (plugin.critical) {
|
|
31070
|
-
console.error(`[Kernel] Failed to bootstrap critical system plugin ${plugin.name}:`, err);
|
|
31071
|
-
throw err;
|
|
31072
|
-
} else {
|
|
31073
|
-
console.warn(`[Kernel] Soft-fail: Failed to bootstrap AI system plugin ${plugin.name}:`, err);
|
|
31074
|
-
}
|
|
31075
|
-
}
|
|
31076
|
-
}
|
|
31077
|
-
try {
|
|
31078
|
-
await this.pluginHost.restoreActivePlugins();
|
|
31079
|
-
} catch (err) {
|
|
31080
|
-
console.error("[Kernel] Failed to restore active plugins:", err);
|
|
31081
|
-
}
|
|
31082
|
-
}
|
|
31083
|
-
/**
|
|
31084
|
-
* Phase 7: 将 DB 中存有 source_code 的旧插件迁移到文件系统(幂等)
|
|
31085
|
-
*/
|
|
31086
|
-
async migratePluginsToFilesystem() {
|
|
31087
|
-
const plugins = this.db.prepare("SELECT id, source_code, manifest FROM plugins WHERE source_code != '' AND source_code IS NOT NULL").all();
|
|
31088
|
-
if (plugins.length === 0) return;
|
|
31089
|
-
console.log(`[Migration] Found ${plugins.length} plugin(s) to migrate to filesystem`);
|
|
31090
|
-
const pluginsDir = this.pluginHost.getPluginsDir();
|
|
31091
|
-
for (const p of plugins) {
|
|
31092
|
-
const pluginDir = import_path12.default.join(pluginsDir, p.id);
|
|
31093
|
-
const indexPath = import_path12.default.join(pluginDir, "index.js");
|
|
31094
|
-
const manifestPath = import_path12.default.join(pluginDir, "manifest.json");
|
|
31095
|
-
if (import_fs13.default.existsSync(indexPath)) {
|
|
31096
|
-
this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
|
|
31097
|
-
continue;
|
|
31098
|
-
}
|
|
31099
|
-
try {
|
|
31100
|
-
import_fs13.default.mkdirSync(pluginDir, { recursive: true });
|
|
31101
|
-
import_fs13.default.writeFileSync(indexPath, p.source_code, "utf-8");
|
|
31102
|
-
if (!import_fs13.default.existsSync(manifestPath)) {
|
|
31103
|
-
import_fs13.default.writeFileSync(manifestPath, p.manifest, "utf-8");
|
|
31104
|
-
}
|
|
31105
|
-
this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
|
|
31106
|
-
console.log(`[Migration] Plugin "${p.id}" migrated to ${indexPath}`);
|
|
31107
|
-
} catch (err) {
|
|
31108
|
-
console.error(`[Migration] Failed to migrate plugin "${p.id}":`, err);
|
|
31109
|
-
}
|
|
31110
|
-
}
|
|
31111
|
-
console.log("[Migration] Plugin migration complete");
|
|
31112
|
-
}
|
|
31113
|
-
// Subscribe to all events and log them to DB
|
|
31114
|
-
initAuditLog() {
|
|
31115
|
-
let stmt = null;
|
|
31116
|
-
let hasLessonIdColumn = null;
|
|
31117
|
-
this.eventBus.subscribe("*", (event) => {
|
|
31118
|
-
const values = [
|
|
31119
|
-
event.id,
|
|
31120
|
-
event.type,
|
|
31121
|
-
event.source,
|
|
31122
|
-
JSON.stringify(redactPasscodeMaterial(event.payload)),
|
|
31123
|
-
event.timestamp,
|
|
31124
|
-
event.correlationId || null
|
|
31125
|
-
];
|
|
31126
|
-
if (hasLessonIdColumn === null) {
|
|
31127
|
-
const columns = this.db.prepare("PRAGMA table_info(events)").all();
|
|
31128
|
-
hasLessonIdColumn = columns.some((c) => c.name === "lesson_id");
|
|
31129
|
-
stmt = this.db.prepare(hasLessonIdColumn ? INSERT_EVENT_WITH_LESSON : INSERT_EVENT_LEGACY);
|
|
31130
|
-
}
|
|
31131
|
-
if (hasLessonIdColumn) {
|
|
31132
|
-
stmt.run(...values, extractEventLessonId(event.payload));
|
|
31133
|
-
} else {
|
|
31134
|
-
stmt.run(...values);
|
|
31135
|
-
}
|
|
31136
|
-
});
|
|
31137
|
-
}
|
|
31138
|
-
};
|
|
31139
|
-
var INSERT_EVENT_WITH_LESSON = "INSERT INTO events (id, type, source, payload, timestamp, correlationId, lesson_id) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
31140
|
-
var INSERT_EVENT_LEGACY = "INSERT INTO events (id, type, source, payload, timestamp, correlationId) VALUES (?, ?, ?, ?, ?, ?)";
|
|
31141
|
-
function extractEventLessonId(payload) {
|
|
31142
|
-
if (!payload || typeof payload !== "object") return null;
|
|
31143
|
-
const record = payload;
|
|
31144
|
-
for (const key of ["lessonId", "lesson_id"]) {
|
|
31145
|
-
const value = record[key];
|
|
31146
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
31147
|
-
}
|
|
31148
|
-
return null;
|
|
31149
|
-
}
|
|
31150
|
-
function resolvePluginsDirOverride() {
|
|
31151
|
-
const raw = process.env.OPENLEARN_PLUGINS_DIR;
|
|
31152
|
-
if (typeof raw === "string" && raw.length > 0) return raw;
|
|
31153
|
-
const dataDir = process.env.OPENLEARN_DATA_DIR?.trim();
|
|
31154
|
-
if (dataDir) return import_path12.default.join(import_path12.default.resolve(dataDir), "plugins");
|
|
31155
|
-
if (process.env.VITEST) {
|
|
31156
|
-
throw new Error(
|
|
31157
|
-
"[H-1] \u6D4B\u8BD5\u73AF\u5883\u4E0B\u672A\u80FD\u786E\u5B9A\u63D2\u4EF6\u76EE\u5F55\uFF0C\u62D2\u7EDD\u56DE\u9000\u5230 process.cwd()/plugins\u3002\n \u539F\u56E0\uFF1A\u56DE\u9000\u4F1A\u628A\u63D2\u4EF6\u4EA7\u7269\u5199\u8FDB\u4ED3\u5E93\u5DE5\u4F5C\u6811\uFF1B\u8BE5\u76EE\u5F55\u88AB .gitignore \u5FFD\u7565\uFF0C\u6C61\u67D3\u5B8C\u5168\u9690\u5F62\u3002\n \u4FEE\u6CD5\uFF08\u62E9\u4E00\uFF09\uFF1A\n 1. \u8BE5\u6D4B\u8BD5\u81EA\u5DF1 new Kernel({ pluginsDir })\uFF0C\u7528 __tests__/helpers/plugins-dir \u7684\n createPluginsDir('\u7528\u9014') \u53D6\u5F97\uFF0C\u5E76\u5728 afterEach \u91CC cleanupPluginsDir\uFF1B\n 2. \u82E5\u7528\u7684\u662F kernelContainer \u5355\u4F8B\uFF0C\u786E\u8BA4\u5B83\u662F\u5728 vitest.setup.ts \u8D4B\u503C\n OPENLEARN_PLUGINS_DIR \u4E4B\u540E\u624D\u88AB\u9996\u6B21\u8BBF\u95EE\u3002"
|
|
31158
|
-
);
|
|
31159
|
-
}
|
|
31160
|
-
return void 0;
|
|
31161
|
-
}
|
|
31162
|
-
var _kernelContainer;
|
|
31163
|
-
function ensureKernelContainer() {
|
|
31164
|
-
if (!_kernelContainer) {
|
|
31165
|
-
_kernelContainer = new Kernel({ pluginsDir: resolvePluginsDirOverride() });
|
|
31166
|
-
_kernelContainer.initAuditLog();
|
|
31167
|
-
}
|
|
31168
|
-
return _kernelContainer;
|
|
31169
|
-
}
|
|
31170
|
-
var kernelContainer = new Proxy({}, {
|
|
31171
|
-
get(target, prop, receiver) {
|
|
31172
|
-
return Reflect.get(ensureKernelContainer(), prop, receiver);
|
|
31173
|
-
},
|
|
31174
|
-
set(target, prop, value, receiver) {
|
|
31175
|
-
return Reflect.set(ensureKernelContainer(), prop, value, receiver);
|
|
31176
|
-
}
|
|
31177
|
-
});
|
|
31178
|
-
function validateJsonSchema(data, schema) {
|
|
31179
|
-
if (!schema) return [];
|
|
31180
|
-
const errors = [];
|
|
31181
|
-
const type = schema.type;
|
|
31182
|
-
if (type === "OBJECT") {
|
|
31183
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
31184
|
-
errors.push(`Expected object, got ${typeof data}`);
|
|
31185
|
-
return errors;
|
|
31186
|
-
}
|
|
31187
|
-
if (schema.required && Array.isArray(schema.required)) {
|
|
31188
|
-
for (const req of schema.required) {
|
|
31189
|
-
if (!(req in data) || data[req] === void 0) {
|
|
31190
|
-
errors.push(`Missing required property "${req}"`);
|
|
31191
|
-
}
|
|
30331
|
+
});
|
|
30332
|
+
function validateJsonSchema(data, schema) {
|
|
30333
|
+
if (!schema) return [];
|
|
30334
|
+
const errors = [];
|
|
30335
|
+
const type = schema.type;
|
|
30336
|
+
if (type === "OBJECT") {
|
|
30337
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
30338
|
+
errors.push(`Expected object, got ${typeof data}`);
|
|
30339
|
+
return errors;
|
|
30340
|
+
}
|
|
30341
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
30342
|
+
for (const req of schema.required) {
|
|
30343
|
+
if (!(req in data) || data[req] === void 0) {
|
|
30344
|
+
errors.push(`Missing required property "${req}"`);
|
|
30345
|
+
}
|
|
31192
30346
|
}
|
|
31193
30347
|
}
|
|
31194
30348
|
if (schema.properties && typeof schema.properties === "object") {
|
|
@@ -32769,6 +31923,299 @@ var import_helmet = __toESM(require("helmet"), 1);
|
|
|
32769
31923
|
|
|
32770
31924
|
// server/security/rate-limit.ts
|
|
32771
31925
|
var import_express_rate_limit = __toESM(require("express-rate-limit"), 1);
|
|
31926
|
+
|
|
31927
|
+
// server/security/route-scope.ts
|
|
31928
|
+
var AUTH_MIDDLEWARE_MARKER = Symbol.for("openlearn.routeScope.authMiddleware");
|
|
31929
|
+
var DATA_SCOPE_MARKER = Symbol.for("openlearn.routeScope.dataScope");
|
|
31930
|
+
function markAuthMiddleware(middleware, roles) {
|
|
31931
|
+
Object.defineProperty(middleware, AUTH_MIDDLEWARE_MARKER, {
|
|
31932
|
+
value: { roles: [...roles] },
|
|
31933
|
+
enumerable: false,
|
|
31934
|
+
configurable: true
|
|
31935
|
+
});
|
|
31936
|
+
return middleware;
|
|
31937
|
+
}
|
|
31938
|
+
function markDataScopeMiddleware(middleware, scope) {
|
|
31939
|
+
Object.defineProperty(middleware, DATA_SCOPE_MARKER, {
|
|
31940
|
+
value: scope,
|
|
31941
|
+
enumerable: false,
|
|
31942
|
+
configurable: true
|
|
31943
|
+
});
|
|
31944
|
+
return middleware;
|
|
31945
|
+
}
|
|
31946
|
+
function readMarker(handle, marker) {
|
|
31947
|
+
if (typeof handle !== "function") return void 0;
|
|
31948
|
+
return handle[marker];
|
|
31949
|
+
}
|
|
31950
|
+
function collectAuthRoutes(app) {
|
|
31951
|
+
const router = app._router ?? app.router;
|
|
31952
|
+
const stack = router?.stack ?? [];
|
|
31953
|
+
const result = [];
|
|
31954
|
+
for (const layer of stack) {
|
|
31955
|
+
const route = layer?.route;
|
|
31956
|
+
if (!route) continue;
|
|
31957
|
+
const handles = (route.stack ?? []).map((entry) => entry.handle);
|
|
31958
|
+
const authMeta = handles.map((handle) => readMarker(handle, AUTH_MIDDLEWARE_MARKER)).find(Boolean);
|
|
31959
|
+
if (!authMeta) continue;
|
|
31960
|
+
const scope = handles.map((handle) => readMarker(handle, DATA_SCOPE_MARKER)).find(Boolean);
|
|
31961
|
+
const methods = Object.keys(route.methods ?? {}).filter((method) => method !== "_all" && route.methods[method]).map((method) => method.toUpperCase());
|
|
31962
|
+
for (const method of methods) {
|
|
31963
|
+
result.push({
|
|
31964
|
+
method,
|
|
31965
|
+
path: String(route.path),
|
|
31966
|
+
scope,
|
|
31967
|
+
roles: [...authMeta.roles]
|
|
31968
|
+
});
|
|
31969
|
+
}
|
|
31970
|
+
}
|
|
31971
|
+
return result;
|
|
31972
|
+
}
|
|
31973
|
+
function isWhitelisted(route, whitelist) {
|
|
31974
|
+
return whitelist.some(
|
|
31975
|
+
(entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
|
|
31976
|
+
);
|
|
31977
|
+
}
|
|
31978
|
+
function normalizePath2(path48) {
|
|
31979
|
+
if (!path48) return "/";
|
|
31980
|
+
return path48.endsWith("/") && path48.length > 1 ? path48.slice(0, -1) : path48;
|
|
31981
|
+
}
|
|
31982
|
+
function auditRouteScopes(app, whitelist = []) {
|
|
31983
|
+
const declared = [];
|
|
31984
|
+
const roleGated = [];
|
|
31985
|
+
const undeclared = [];
|
|
31986
|
+
for (const route of collectAuthRoutes(app)) {
|
|
31987
|
+
if (route.scope) {
|
|
31988
|
+
declared.push(route);
|
|
31989
|
+
continue;
|
|
31990
|
+
}
|
|
31991
|
+
if (route.roles.length > 0) {
|
|
31992
|
+
roleGated.push(route);
|
|
31993
|
+
continue;
|
|
31994
|
+
}
|
|
31995
|
+
if (isWhitelisted(route, whitelist)) continue;
|
|
31996
|
+
undeclared.push(route);
|
|
31997
|
+
}
|
|
31998
|
+
return { declared, roleGated, undeclared };
|
|
31999
|
+
}
|
|
32000
|
+
function formatRouteScopeViolation(route) {
|
|
32001
|
+
return `${route.method} ${route.path}`;
|
|
32002
|
+
}
|
|
32003
|
+
function assertRouteDataScopes(app, options = {}) {
|
|
32004
|
+
const logger2 = options.logger ?? console;
|
|
32005
|
+
const audit = auditRouteScopes(app, options.whitelist ?? []);
|
|
32006
|
+
if (audit.undeclared.length > 0) {
|
|
32007
|
+
const lines = audit.undeclared.map(formatRouteScopeViolation);
|
|
32008
|
+
const message = `[RouteScope] ${audit.undeclared.length} \u4E2A\u5DF2\u8BA4\u8BC1\u8DEF\u7531\u672A\u58F0\u660E\u6570\u636E\u8303\u56F4\uFF08requireScopedAuth\uFF09\uFF1A
|
|
32009
|
+
- ${lines.join("\n - ")}`;
|
|
32010
|
+
if (options.enforce) {
|
|
32011
|
+
logger2.error(message);
|
|
32012
|
+
throw new Error(message);
|
|
32013
|
+
}
|
|
32014
|
+
logger2.warn(message);
|
|
32015
|
+
}
|
|
32016
|
+
if (audit.declared.length > 0) {
|
|
32017
|
+
logger2.info?.(
|
|
32018
|
+
`[RouteScope] \u5DF2\u58F0\u660E\u6570\u636E\u8303\u56F4 ${audit.declared.length} \u6761\uFF0C\u89D2\u8272\u95E8\u7981\u8DEF\u7531 ${audit.roleGated.length} \u6761\uFF0C\u672A\u58F0\u660E ${audit.undeclared.length} \u6761\uFF08\u767D\u540D\u5355 ${options.whitelist?.length ?? 0} \u6761\uFF09\u3002`
|
|
32019
|
+
);
|
|
32020
|
+
}
|
|
32021
|
+
return audit;
|
|
32022
|
+
}
|
|
32023
|
+
|
|
32024
|
+
// server/utils/password-gate.ts
|
|
32025
|
+
var PASSWORD_GATE_SKIP_ENV = "OPENLEARN_SKIP_PASSWORD_CHANGE_GATE";
|
|
32026
|
+
var PASSWORD_GATE_SKIP_ENV_ALIAS = "DEV_BYPASS_PASSWORD_CHANGE_GATE";
|
|
32027
|
+
var productionWarningEmitted = false;
|
|
32028
|
+
function isTruthyFlag2(value) {
|
|
32029
|
+
if (typeof value !== "string") return false;
|
|
32030
|
+
const normalized = value.trim().toLowerCase();
|
|
32031
|
+
return normalized === "1" || normalized === "true";
|
|
32032
|
+
}
|
|
32033
|
+
function isPasswordChangeGateSkipped(env = process.env) {
|
|
32034
|
+
const requested = isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV]) || isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV_ALIAS]);
|
|
32035
|
+
if (!requested) return false;
|
|
32036
|
+
if ((env.NODE_ENV ?? "") === "production") {
|
|
32037
|
+
if (!productionWarningEmitted) {
|
|
32038
|
+
productionWarningEmitted = true;
|
|
32039
|
+
console.warn(
|
|
32040
|
+
`[SECURITY] ${PASSWORD_GATE_SKIP_ENV}/${PASSWORD_GATE_SKIP_ENV_ALIAS} \u5DF2\u914D\u7F6E\uFF0C\u4F46\u751F\u4EA7\u73AF\u5883\uFF08NODE_ENV=production\uFF09\u4E25\u683C\u5FFD\u7565\u8BE5\u5F00\u5173\uFF1A\u5F3A\u5236\u6539\u5BC6\u9632\u7EBF\u4E0D\u5931\u6548\u3002\u8BF7\u7ACB\u5373\u4ECE\u751F\u4EA7\u73AF\u5883\u79FB\u9664\u8BE5\u53D8\u91CF\u3002`
|
|
32041
|
+
);
|
|
32042
|
+
}
|
|
32043
|
+
return false;
|
|
32044
|
+
}
|
|
32045
|
+
return true;
|
|
32046
|
+
}
|
|
32047
|
+
|
|
32048
|
+
// server/services/session-role.ts
|
|
32049
|
+
var SESSION_ROLE_WHITELIST = /* @__PURE__ */ new Set(["administrator", "teacher", "student", "anonymous"]);
|
|
32050
|
+
function resolveSessionRole(session) {
|
|
32051
|
+
if (!session) return "anonymous";
|
|
32052
|
+
for (const candidate of [session.subRole, session.role]) {
|
|
32053
|
+
if (typeof candidate !== "string" || candidate.length === 0) continue;
|
|
32054
|
+
const normalized = candidate === "admin" ? "administrator" : candidate;
|
|
32055
|
+
if (SESSION_ROLE_WHITELIST.has(normalized)) return normalized;
|
|
32056
|
+
}
|
|
32057
|
+
return "anonymous";
|
|
32058
|
+
}
|
|
32059
|
+
function isAdministrator(session) {
|
|
32060
|
+
return resolveSessionRole(session) === "administrator";
|
|
32061
|
+
}
|
|
32062
|
+
|
|
32063
|
+
// server/middleware/auth.ts
|
|
32064
|
+
var SESSION_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
32065
|
+
function getValidSession(token) {
|
|
32066
|
+
let sessionRow;
|
|
32067
|
+
try {
|
|
32068
|
+
sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
32069
|
+
} catch {
|
|
32070
|
+
return null;
|
|
32071
|
+
}
|
|
32072
|
+
if (!sessionRow) return null;
|
|
32073
|
+
const now = Date.now();
|
|
32074
|
+
if (sessionRow.expires_at && sessionRow.expires_at < now) {
|
|
32075
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
32076
|
+
return null;
|
|
32077
|
+
}
|
|
32078
|
+
const idleTimeout = 24 * 60 * 60 * 1e3;
|
|
32079
|
+
if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
|
|
32080
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
32081
|
+
return null;
|
|
32082
|
+
}
|
|
32083
|
+
if (!sessionRow.updated_at || now - sessionRow.updated_at > SESSION_REFRESH_INTERVAL_MS) {
|
|
32084
|
+
kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
|
|
32085
|
+
}
|
|
32086
|
+
let session;
|
|
32087
|
+
try {
|
|
32088
|
+
session = JSON.parse(sessionRow.session_data);
|
|
32089
|
+
} catch {
|
|
32090
|
+
return null;
|
|
32091
|
+
}
|
|
32092
|
+
if (session && !session.userId && session.studentId) {
|
|
32093
|
+
session.userId = session.studentId;
|
|
32094
|
+
}
|
|
32095
|
+
if (session.role && !["teacher", "student", "administrator", "admin"].includes(session.role)) {
|
|
32096
|
+
session.role = "anonymous";
|
|
32097
|
+
}
|
|
32098
|
+
return session;
|
|
32099
|
+
}
|
|
32100
|
+
function getCookieToken(req) {
|
|
32101
|
+
const rc = req.headers.cookie;
|
|
32102
|
+
if (!rc) return null;
|
|
32103
|
+
const parts = rc.split(";");
|
|
32104
|
+
for (const part of parts) {
|
|
32105
|
+
const trimmed2 = part.trim();
|
|
32106
|
+
if (trimmed2.startsWith("edu_os_token=")) {
|
|
32107
|
+
return trimmed2.substring("edu_os_token=".length);
|
|
32108
|
+
}
|
|
32109
|
+
}
|
|
32110
|
+
return null;
|
|
32111
|
+
}
|
|
32112
|
+
function getActorId(req) {
|
|
32113
|
+
const token = getCookieToken(req);
|
|
32114
|
+
if (!token) return "anonymous";
|
|
32115
|
+
try {
|
|
32116
|
+
const session = getValidSession(token);
|
|
32117
|
+
if (!session) return "anonymous";
|
|
32118
|
+
const role = resolveSessionRole(session);
|
|
32119
|
+
const rawUserId = session.userId || session.studentId || "demo";
|
|
32120
|
+
const safeUserId = String(rawUserId).replace(/[:\s]/g, "_").slice(0, 64);
|
|
32121
|
+
return `user:${safeUserId}:${role}`;
|
|
32122
|
+
} catch {
|
|
32123
|
+
return "anonymous";
|
|
32124
|
+
}
|
|
32125
|
+
}
|
|
32126
|
+
function requireAuth(...roles) {
|
|
32127
|
+
const middleware = (req, res, next) => {
|
|
32128
|
+
const token = getCookieToken(req);
|
|
32129
|
+
if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
|
|
32130
|
+
const session = getValidSession(token);
|
|
32131
|
+
if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
|
|
32132
|
+
if (roles.length > 0) {
|
|
32133
|
+
const userRole = resolveSessionRole(session);
|
|
32134
|
+
const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
|
|
32135
|
+
if (!effectiveRoles.includes(userRole)) {
|
|
32136
|
+
return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
|
|
32137
|
+
}
|
|
32138
|
+
}
|
|
32139
|
+
req.session = session;
|
|
32140
|
+
next();
|
|
32141
|
+
};
|
|
32142
|
+
return markAuthMiddleware(middleware, roles);
|
|
32143
|
+
}
|
|
32144
|
+
var PASSWORD_CHANGE_EXEMPT_PATHS = /* @__PURE__ */ new Set([
|
|
32145
|
+
"/api/auth/change-password",
|
|
32146
|
+
"/api/auth/logout",
|
|
32147
|
+
"/api/auth/session",
|
|
32148
|
+
"/api/auth/me",
|
|
32149
|
+
"/api/client-errors"
|
|
32150
|
+
]);
|
|
32151
|
+
function enforcePasswordChanged(req, res, next) {
|
|
32152
|
+
if (process.env.PLAYWRIGHT_TEST) {
|
|
32153
|
+
return next();
|
|
32154
|
+
}
|
|
32155
|
+
if (isPasswordChangeGateSkipped()) {
|
|
32156
|
+
return next();
|
|
32157
|
+
}
|
|
32158
|
+
const path48 = req.path || "";
|
|
32159
|
+
if (!path48.startsWith("/api/")) {
|
|
32160
|
+
return next();
|
|
32161
|
+
}
|
|
32162
|
+
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path48)) {
|
|
32163
|
+
return next();
|
|
32164
|
+
}
|
|
32165
|
+
const token = getCookieToken(req);
|
|
32166
|
+
if (token) {
|
|
32167
|
+
const session = getValidSession(token);
|
|
32168
|
+
if (session && session.mustChangePassword) {
|
|
32169
|
+
res.status(403).json({
|
|
32170
|
+
success: false,
|
|
32171
|
+
code: "FORBIDDEN_DEFAULT_PASSWORD",
|
|
32172
|
+
error: "Default password must be changed first"
|
|
32173
|
+
});
|
|
32174
|
+
return;
|
|
32175
|
+
}
|
|
32176
|
+
}
|
|
32177
|
+
next();
|
|
32178
|
+
}
|
|
32179
|
+
function socketAuthMiddleware(socket, next) {
|
|
32180
|
+
try {
|
|
32181
|
+
const cookieHeader = socket.handshake.headers.cookie;
|
|
32182
|
+
let token = null;
|
|
32183
|
+
if (cookieHeader) {
|
|
32184
|
+
const parts = cookieHeader.split(";");
|
|
32185
|
+
for (const part of parts) {
|
|
32186
|
+
const trimmed2 = part.trim();
|
|
32187
|
+
if (trimmed2.startsWith("edu_os_token=")) {
|
|
32188
|
+
token = trimmed2.substring("edu_os_token=".length);
|
|
32189
|
+
break;
|
|
32190
|
+
}
|
|
32191
|
+
}
|
|
32192
|
+
}
|
|
32193
|
+
if (!token && socket.handshake.auth?.token) {
|
|
32194
|
+
token = socket.handshake.auth.token;
|
|
32195
|
+
}
|
|
32196
|
+
if (process.env.NODE_ENV === "test" && !token) {
|
|
32197
|
+
return next();
|
|
32198
|
+
}
|
|
32199
|
+
if (!token) {
|
|
32200
|
+
return next(new Error("Authentication required: missing edu_os_token"));
|
|
32201
|
+
}
|
|
32202
|
+
const session = getValidSession(token);
|
|
32203
|
+
if (!session) {
|
|
32204
|
+
return next(new Error("Authentication required: session expired or invalid"));
|
|
32205
|
+
}
|
|
32206
|
+
if (session.mustChangePassword && !isPasswordChangeGateSkipped()) {
|
|
32207
|
+
return next(new Error("FORBIDDEN_DEFAULT_PASSWORD"));
|
|
32208
|
+
}
|
|
32209
|
+
socket.data.session = session;
|
|
32210
|
+
socket.data.userId = session.userId;
|
|
32211
|
+
socket.data.role = session.role;
|
|
32212
|
+
next();
|
|
32213
|
+
} catch (err) {
|
|
32214
|
+
next(new Error(`Authentication error: ${err.message}`));
|
|
32215
|
+
}
|
|
32216
|
+
}
|
|
32217
|
+
|
|
32218
|
+
// server/security/rate-limit.ts
|
|
32772
32219
|
function loginRateLimitKey(req) {
|
|
32773
32220
|
const body = req.body ?? {};
|
|
32774
32221
|
const raw = body.username ?? body.studentId ?? body.studentName ?? "";
|
|
@@ -32785,6 +32232,18 @@ function writeRateLimitKey(req) {
|
|
|
32785
32232
|
return ip;
|
|
32786
32233
|
}
|
|
32787
32234
|
}
|
|
32235
|
+
function sessionRateLimitKey(req) {
|
|
32236
|
+
try {
|
|
32237
|
+
const token = getCookieToken(req);
|
|
32238
|
+
const session = token ? getValidSession(token) : null;
|
|
32239
|
+
if (session) {
|
|
32240
|
+
const identity = String(session.userId || session.studentId || session.username || "").trim();
|
|
32241
|
+
if (identity) return `session:${identity.slice(0, 64)}`;
|
|
32242
|
+
}
|
|
32243
|
+
} catch {
|
|
32244
|
+
}
|
|
32245
|
+
return `ip:${writeRateLimitKey(req)}`;
|
|
32246
|
+
}
|
|
32788
32247
|
var limiterValidation = {
|
|
32789
32248
|
xForwardedForHeader: false,
|
|
32790
32249
|
trustProxy: false,
|
|
@@ -32823,6 +32282,17 @@ function createAiLimiter(max) {
|
|
|
32823
32282
|
validate: limiterValidation
|
|
32824
32283
|
});
|
|
32825
32284
|
}
|
|
32285
|
+
function createSessionWriteLimiter(max) {
|
|
32286
|
+
return (0, import_express_rate_limit.default)({
|
|
32287
|
+
windowMs: 60 * 1e3,
|
|
32288
|
+
max,
|
|
32289
|
+
keyGenerator: sessionRateLimitKey,
|
|
32290
|
+
message: { error: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002Too many requests, please try again later." },
|
|
32291
|
+
standardHeaders: true,
|
|
32292
|
+
legacyHeaders: false,
|
|
32293
|
+
validate: limiterValidation
|
|
32294
|
+
});
|
|
32295
|
+
}
|
|
32826
32296
|
|
|
32827
32297
|
// server/security/trust-proxy.ts
|
|
32828
32298
|
function resolveTrustProxy(raw) {
|
|
@@ -34199,297 +33669,6 @@ function startBackupScheduler(deps) {
|
|
|
34199
33669
|
};
|
|
34200
33670
|
}
|
|
34201
33671
|
|
|
34202
|
-
// server/security/route-scope.ts
|
|
34203
|
-
var AUTH_MIDDLEWARE_MARKER = Symbol.for("openlearn.routeScope.authMiddleware");
|
|
34204
|
-
var DATA_SCOPE_MARKER = Symbol.for("openlearn.routeScope.dataScope");
|
|
34205
|
-
function markAuthMiddleware(middleware, roles) {
|
|
34206
|
-
Object.defineProperty(middleware, AUTH_MIDDLEWARE_MARKER, {
|
|
34207
|
-
value: { roles: [...roles] },
|
|
34208
|
-
enumerable: false,
|
|
34209
|
-
configurable: true
|
|
34210
|
-
});
|
|
34211
|
-
return middleware;
|
|
34212
|
-
}
|
|
34213
|
-
function markDataScopeMiddleware(middleware, scope) {
|
|
34214
|
-
Object.defineProperty(middleware, DATA_SCOPE_MARKER, {
|
|
34215
|
-
value: scope,
|
|
34216
|
-
enumerable: false,
|
|
34217
|
-
configurable: true
|
|
34218
|
-
});
|
|
34219
|
-
return middleware;
|
|
34220
|
-
}
|
|
34221
|
-
function readMarker(handle, marker) {
|
|
34222
|
-
if (typeof handle !== "function") return void 0;
|
|
34223
|
-
return handle[marker];
|
|
34224
|
-
}
|
|
34225
|
-
function collectAuthRoutes(app) {
|
|
34226
|
-
const router = app._router ?? app.router;
|
|
34227
|
-
const stack = router?.stack ?? [];
|
|
34228
|
-
const result = [];
|
|
34229
|
-
for (const layer of stack) {
|
|
34230
|
-
const route = layer?.route;
|
|
34231
|
-
if (!route) continue;
|
|
34232
|
-
const handles = (route.stack ?? []).map((entry) => entry.handle);
|
|
34233
|
-
const authMeta = handles.map((handle) => readMarker(handle, AUTH_MIDDLEWARE_MARKER)).find(Boolean);
|
|
34234
|
-
if (!authMeta) continue;
|
|
34235
|
-
const scope = handles.map((handle) => readMarker(handle, DATA_SCOPE_MARKER)).find(Boolean);
|
|
34236
|
-
const methods = Object.keys(route.methods ?? {}).filter((method) => method !== "_all" && route.methods[method]).map((method) => method.toUpperCase());
|
|
34237
|
-
for (const method of methods) {
|
|
34238
|
-
result.push({
|
|
34239
|
-
method,
|
|
34240
|
-
path: String(route.path),
|
|
34241
|
-
scope,
|
|
34242
|
-
roles: [...authMeta.roles]
|
|
34243
|
-
});
|
|
34244
|
-
}
|
|
34245
|
-
}
|
|
34246
|
-
return result;
|
|
34247
|
-
}
|
|
34248
|
-
function isWhitelisted(route, whitelist) {
|
|
34249
|
-
return whitelist.some(
|
|
34250
|
-
(entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
|
|
34251
|
-
);
|
|
34252
|
-
}
|
|
34253
|
-
function normalizePath2(path48) {
|
|
34254
|
-
if (!path48) return "/";
|
|
34255
|
-
return path48.endsWith("/") && path48.length > 1 ? path48.slice(0, -1) : path48;
|
|
34256
|
-
}
|
|
34257
|
-
function auditRouteScopes(app, whitelist = []) {
|
|
34258
|
-
const declared = [];
|
|
34259
|
-
const roleGated = [];
|
|
34260
|
-
const undeclared = [];
|
|
34261
|
-
for (const route of collectAuthRoutes(app)) {
|
|
34262
|
-
if (route.scope) {
|
|
34263
|
-
declared.push(route);
|
|
34264
|
-
continue;
|
|
34265
|
-
}
|
|
34266
|
-
if (route.roles.length > 0) {
|
|
34267
|
-
roleGated.push(route);
|
|
34268
|
-
continue;
|
|
34269
|
-
}
|
|
34270
|
-
if (isWhitelisted(route, whitelist)) continue;
|
|
34271
|
-
undeclared.push(route);
|
|
34272
|
-
}
|
|
34273
|
-
return { declared, roleGated, undeclared };
|
|
34274
|
-
}
|
|
34275
|
-
function formatRouteScopeViolation(route) {
|
|
34276
|
-
return `${route.method} ${route.path}`;
|
|
34277
|
-
}
|
|
34278
|
-
function assertRouteDataScopes(app, options = {}) {
|
|
34279
|
-
const logger2 = options.logger ?? console;
|
|
34280
|
-
const audit = auditRouteScopes(app, options.whitelist ?? []);
|
|
34281
|
-
if (audit.undeclared.length > 0) {
|
|
34282
|
-
const lines = audit.undeclared.map(formatRouteScopeViolation);
|
|
34283
|
-
const message = `[RouteScope] ${audit.undeclared.length} \u4E2A\u5DF2\u8BA4\u8BC1\u8DEF\u7531\u672A\u58F0\u660E\u6570\u636E\u8303\u56F4\uFF08requireScopedAuth\uFF09\uFF1A
|
|
34284
|
-
- ${lines.join("\n - ")}`;
|
|
34285
|
-
if (options.enforce) {
|
|
34286
|
-
logger2.error(message);
|
|
34287
|
-
throw new Error(message);
|
|
34288
|
-
}
|
|
34289
|
-
logger2.warn(message);
|
|
34290
|
-
}
|
|
34291
|
-
if (audit.declared.length > 0) {
|
|
34292
|
-
logger2.info?.(
|
|
34293
|
-
`[RouteScope] \u5DF2\u58F0\u660E\u6570\u636E\u8303\u56F4 ${audit.declared.length} \u6761\uFF0C\u89D2\u8272\u95E8\u7981\u8DEF\u7531 ${audit.roleGated.length} \u6761\uFF0C\u672A\u58F0\u660E ${audit.undeclared.length} \u6761\uFF08\u767D\u540D\u5355 ${options.whitelist?.length ?? 0} \u6761\uFF09\u3002`
|
|
34294
|
-
);
|
|
34295
|
-
}
|
|
34296
|
-
return audit;
|
|
34297
|
-
}
|
|
34298
|
-
|
|
34299
|
-
// server/utils/password-gate.ts
|
|
34300
|
-
var PASSWORD_GATE_SKIP_ENV = "OPENLEARN_SKIP_PASSWORD_CHANGE_GATE";
|
|
34301
|
-
var PASSWORD_GATE_SKIP_ENV_ALIAS = "DEV_BYPASS_PASSWORD_CHANGE_GATE";
|
|
34302
|
-
var productionWarningEmitted = false;
|
|
34303
|
-
function isTruthyFlag2(value) {
|
|
34304
|
-
if (typeof value !== "string") return false;
|
|
34305
|
-
const normalized = value.trim().toLowerCase();
|
|
34306
|
-
return normalized === "1" || normalized === "true";
|
|
34307
|
-
}
|
|
34308
|
-
function isPasswordChangeGateSkipped(env = process.env) {
|
|
34309
|
-
const requested = isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV]) || isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV_ALIAS]);
|
|
34310
|
-
if (!requested) return false;
|
|
34311
|
-
if ((env.NODE_ENV ?? "") === "production") {
|
|
34312
|
-
if (!productionWarningEmitted) {
|
|
34313
|
-
productionWarningEmitted = true;
|
|
34314
|
-
console.warn(
|
|
34315
|
-
`[SECURITY] ${PASSWORD_GATE_SKIP_ENV}/${PASSWORD_GATE_SKIP_ENV_ALIAS} \u5DF2\u914D\u7F6E\uFF0C\u4F46\u751F\u4EA7\u73AF\u5883\uFF08NODE_ENV=production\uFF09\u4E25\u683C\u5FFD\u7565\u8BE5\u5F00\u5173\uFF1A\u5F3A\u5236\u6539\u5BC6\u9632\u7EBF\u4E0D\u5931\u6548\u3002\u8BF7\u7ACB\u5373\u4ECE\u751F\u4EA7\u73AF\u5883\u79FB\u9664\u8BE5\u53D8\u91CF\u3002`
|
|
34316
|
-
);
|
|
34317
|
-
}
|
|
34318
|
-
return false;
|
|
34319
|
-
}
|
|
34320
|
-
return true;
|
|
34321
|
-
}
|
|
34322
|
-
|
|
34323
|
-
// server/services/session-role.ts
|
|
34324
|
-
var SESSION_ROLE_WHITELIST = /* @__PURE__ */ new Set(["administrator", "teacher", "student", "anonymous"]);
|
|
34325
|
-
function resolveSessionRole(session) {
|
|
34326
|
-
if (!session) return "anonymous";
|
|
34327
|
-
for (const candidate of [session.subRole, session.role]) {
|
|
34328
|
-
if (typeof candidate !== "string" || candidate.length === 0) continue;
|
|
34329
|
-
const normalized = candidate === "admin" ? "administrator" : candidate;
|
|
34330
|
-
if (SESSION_ROLE_WHITELIST.has(normalized)) return normalized;
|
|
34331
|
-
}
|
|
34332
|
-
return "anonymous";
|
|
34333
|
-
}
|
|
34334
|
-
function isAdministrator(session) {
|
|
34335
|
-
return resolveSessionRole(session) === "administrator";
|
|
34336
|
-
}
|
|
34337
|
-
|
|
34338
|
-
// server/middleware/auth.ts
|
|
34339
|
-
var SESSION_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
34340
|
-
function getValidSession(token) {
|
|
34341
|
-
let sessionRow;
|
|
34342
|
-
try {
|
|
34343
|
-
sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
34344
|
-
} catch {
|
|
34345
|
-
return null;
|
|
34346
|
-
}
|
|
34347
|
-
if (!sessionRow) return null;
|
|
34348
|
-
const now = Date.now();
|
|
34349
|
-
if (sessionRow.expires_at && sessionRow.expires_at < now) {
|
|
34350
|
-
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
34351
|
-
return null;
|
|
34352
|
-
}
|
|
34353
|
-
const idleTimeout = 24 * 60 * 60 * 1e3;
|
|
34354
|
-
if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
|
|
34355
|
-
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
34356
|
-
return null;
|
|
34357
|
-
}
|
|
34358
|
-
if (!sessionRow.updated_at || now - sessionRow.updated_at > SESSION_REFRESH_INTERVAL_MS) {
|
|
34359
|
-
kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
|
|
34360
|
-
}
|
|
34361
|
-
let session;
|
|
34362
|
-
try {
|
|
34363
|
-
session = JSON.parse(sessionRow.session_data);
|
|
34364
|
-
} catch {
|
|
34365
|
-
return null;
|
|
34366
|
-
}
|
|
34367
|
-
if (session && !session.userId && session.studentId) {
|
|
34368
|
-
session.userId = session.studentId;
|
|
34369
|
-
}
|
|
34370
|
-
if (session.role && !["teacher", "student", "administrator", "admin"].includes(session.role)) {
|
|
34371
|
-
session.role = "anonymous";
|
|
34372
|
-
}
|
|
34373
|
-
return session;
|
|
34374
|
-
}
|
|
34375
|
-
function getCookieToken(req) {
|
|
34376
|
-
const rc = req.headers.cookie;
|
|
34377
|
-
if (!rc) return null;
|
|
34378
|
-
const parts = rc.split(";");
|
|
34379
|
-
for (const part of parts) {
|
|
34380
|
-
const trimmed2 = part.trim();
|
|
34381
|
-
if (trimmed2.startsWith("edu_os_token=")) {
|
|
34382
|
-
return trimmed2.substring("edu_os_token=".length);
|
|
34383
|
-
}
|
|
34384
|
-
}
|
|
34385
|
-
return null;
|
|
34386
|
-
}
|
|
34387
|
-
function getActorId(req) {
|
|
34388
|
-
const token = getCookieToken(req);
|
|
34389
|
-
if (!token) return "anonymous";
|
|
34390
|
-
try {
|
|
34391
|
-
const session = getValidSession(token);
|
|
34392
|
-
if (!session) return "anonymous";
|
|
34393
|
-
const role = resolveSessionRole(session);
|
|
34394
|
-
const rawUserId = session.userId || session.studentId || "demo";
|
|
34395
|
-
const safeUserId = String(rawUserId).replace(/[:\s]/g, "_").slice(0, 64);
|
|
34396
|
-
return `user:${safeUserId}:${role}`;
|
|
34397
|
-
} catch {
|
|
34398
|
-
return "anonymous";
|
|
34399
|
-
}
|
|
34400
|
-
}
|
|
34401
|
-
function requireAuth(...roles) {
|
|
34402
|
-
const middleware = (req, res, next) => {
|
|
34403
|
-
const token = getCookieToken(req);
|
|
34404
|
-
if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
|
|
34405
|
-
const session = getValidSession(token);
|
|
34406
|
-
if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
|
|
34407
|
-
if (roles.length > 0) {
|
|
34408
|
-
const userRole = resolveSessionRole(session);
|
|
34409
|
-
const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
|
|
34410
|
-
if (!effectiveRoles.includes(userRole)) {
|
|
34411
|
-
return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
|
|
34412
|
-
}
|
|
34413
|
-
}
|
|
34414
|
-
req.session = session;
|
|
34415
|
-
next();
|
|
34416
|
-
};
|
|
34417
|
-
return markAuthMiddleware(middleware, roles);
|
|
34418
|
-
}
|
|
34419
|
-
var PASSWORD_CHANGE_EXEMPT_PATHS = /* @__PURE__ */ new Set([
|
|
34420
|
-
"/api/auth/change-password",
|
|
34421
|
-
"/api/auth/logout",
|
|
34422
|
-
"/api/auth/session",
|
|
34423
|
-
"/api/auth/me",
|
|
34424
|
-
"/api/client-errors"
|
|
34425
|
-
]);
|
|
34426
|
-
function enforcePasswordChanged(req, res, next) {
|
|
34427
|
-
if (process.env.PLAYWRIGHT_TEST) {
|
|
34428
|
-
return next();
|
|
34429
|
-
}
|
|
34430
|
-
if (isPasswordChangeGateSkipped()) {
|
|
34431
|
-
return next();
|
|
34432
|
-
}
|
|
34433
|
-
const path48 = req.path || "";
|
|
34434
|
-
if (!path48.startsWith("/api/")) {
|
|
34435
|
-
return next();
|
|
34436
|
-
}
|
|
34437
|
-
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path48)) {
|
|
34438
|
-
return next();
|
|
34439
|
-
}
|
|
34440
|
-
const token = getCookieToken(req);
|
|
34441
|
-
if (token) {
|
|
34442
|
-
const session = getValidSession(token);
|
|
34443
|
-
if (session && session.mustChangePassword) {
|
|
34444
|
-
res.status(403).json({
|
|
34445
|
-
success: false,
|
|
34446
|
-
code: "FORBIDDEN_DEFAULT_PASSWORD",
|
|
34447
|
-
error: "Default password must be changed first"
|
|
34448
|
-
});
|
|
34449
|
-
return;
|
|
34450
|
-
}
|
|
34451
|
-
}
|
|
34452
|
-
next();
|
|
34453
|
-
}
|
|
34454
|
-
function socketAuthMiddleware(socket, next) {
|
|
34455
|
-
try {
|
|
34456
|
-
const cookieHeader = socket.handshake.headers.cookie;
|
|
34457
|
-
let token = null;
|
|
34458
|
-
if (cookieHeader) {
|
|
34459
|
-
const parts = cookieHeader.split(";");
|
|
34460
|
-
for (const part of parts) {
|
|
34461
|
-
const trimmed2 = part.trim();
|
|
34462
|
-
if (trimmed2.startsWith("edu_os_token=")) {
|
|
34463
|
-
token = trimmed2.substring("edu_os_token=".length);
|
|
34464
|
-
break;
|
|
34465
|
-
}
|
|
34466
|
-
}
|
|
34467
|
-
}
|
|
34468
|
-
if (!token && socket.handshake.auth?.token) {
|
|
34469
|
-
token = socket.handshake.auth.token;
|
|
34470
|
-
}
|
|
34471
|
-
if (process.env.NODE_ENV === "test" && !token) {
|
|
34472
|
-
return next();
|
|
34473
|
-
}
|
|
34474
|
-
if (!token) {
|
|
34475
|
-
return next(new Error("Authentication required: missing edu_os_token"));
|
|
34476
|
-
}
|
|
34477
|
-
const session = getValidSession(token);
|
|
34478
|
-
if (!session) {
|
|
34479
|
-
return next(new Error("Authentication required: session expired or invalid"));
|
|
34480
|
-
}
|
|
34481
|
-
if (session.mustChangePassword && !isPasswordChangeGateSkipped()) {
|
|
34482
|
-
return next(new Error("FORBIDDEN_DEFAULT_PASSWORD"));
|
|
34483
|
-
}
|
|
34484
|
-
socket.data.session = session;
|
|
34485
|
-
socket.data.userId = session.userId;
|
|
34486
|
-
socket.data.role = session.role;
|
|
34487
|
-
next();
|
|
34488
|
-
} catch (err) {
|
|
34489
|
-
next(new Error(`Authentication error: ${err.message}`));
|
|
34490
|
-
}
|
|
34491
|
-
}
|
|
34492
|
-
|
|
34493
33672
|
// server/middleware/csrf.ts
|
|
34494
33673
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
34495
33674
|
var EXEMPT_PREFIXES = [
|
|
@@ -34712,7 +33891,7 @@ function sendSafeError(res, err, status = 500, fallbackMessage = "Internal serve
|
|
|
34712
33891
|
}
|
|
34713
33892
|
const isProd = process.env.NODE_ENV === "production";
|
|
34714
33893
|
const exposedMessage = isProd && effectiveStatus >= 500 ? fallbackMessage : message;
|
|
34715
|
-
const rawCode = err && typeof err === "object" && typeof err.code === "string" ? err.code : void 0;
|
|
33894
|
+
const rawCode = err && typeof err === "object" && typeof err.code === "string" ? err.code : err && typeof err === "object" && err.type === "entity.too.large" ? "PAYLOAD_TOO_LARGE" : void 0;
|
|
34716
33895
|
const errorCode = effectiveStatus >= 400 && effectiveStatus < 500 && rawCode ? rawCode : void 0;
|
|
34717
33896
|
return res.status(effectiveStatus).json({ success: false, ...errorCode ? { code: errorCode } : {}, error: exposedMessage });
|
|
34718
33897
|
}
|
|
@@ -36170,15 +35349,28 @@ var LARGE_UPLOAD_ENDPOINTS = [
|
|
|
36170
35349
|
"/api/upload",
|
|
36171
35350
|
"/api/courseware/upload",
|
|
36172
35351
|
"/api/courseware/inline",
|
|
36173
|
-
"/api/plugins/upload-zip"
|
|
35352
|
+
"/api/plugins/upload-zip",
|
|
35353
|
+
"/api/resources"
|
|
36174
35354
|
];
|
|
36175
35355
|
var LARGE_UPLOAD_LIMIT = "110mb";
|
|
35356
|
+
var MEDIUM_UPLOAD_ENDPOINTS = [
|
|
35357
|
+
"/api/auth/avatar",
|
|
35358
|
+
"/api/timetable/ocr",
|
|
35359
|
+
"/api/site-settings",
|
|
35360
|
+
"/api/agent/chat"
|
|
35361
|
+
];
|
|
35362
|
+
var MEDIUM_UPLOAD_LIMIT = "30mb";
|
|
36176
35363
|
function mountLargeBodyParsers(app) {
|
|
36177
35364
|
app.use(
|
|
36178
35365
|
LARGE_UPLOAD_ENDPOINTS,
|
|
36179
35366
|
import_express2.default.json({ limit: LARGE_UPLOAD_LIMIT }),
|
|
36180
35367
|
import_express2.default.urlencoded({ limit: LARGE_UPLOAD_LIMIT, extended: true })
|
|
36181
35368
|
);
|
|
35369
|
+
app.use(
|
|
35370
|
+
MEDIUM_UPLOAD_ENDPOINTS,
|
|
35371
|
+
import_express2.default.json({ limit: MEDIUM_UPLOAD_LIMIT }),
|
|
35372
|
+
import_express2.default.urlencoded({ limit: MEDIUM_UPLOAD_LIMIT, extended: true })
|
|
35373
|
+
);
|
|
36182
35374
|
app.use(
|
|
36183
35375
|
COURSE_PACKAGE_IMPORT_PATH,
|
|
36184
35376
|
import_express2.default.json({ limit: COURSE_PACKAGE_IMPORT_BODY_LIMIT_BYTES }),
|
|
@@ -36262,164 +35454,706 @@ window.addEventListener('message', async function (event) {
|
|
|
36262
35454
|
post({ type: 'deactivated', id: data.id });
|
|
36263
35455
|
return;
|
|
36264
35456
|
}
|
|
36265
|
-
if (data.type !== 'load') return;
|
|
36266
|
-
targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
|
|
36267
|
-
try {
|
|
36268
|
-
const blob = new Blob([data.source], { type: 'text/javascript' });
|
|
36269
|
-
const url = URL.createObjectURL(blob);
|
|
36270
|
-
let mod;
|
|
36271
|
-
try { mod = await import(url); } finally { URL.revokeObjectURL(url); }
|
|
36272
|
-
const plugin = mod.default || mod;
|
|
36273
|
-
if (typeof plugin.activate !== 'function') throw new Error('missing activate function');
|
|
36274
|
-
activePlugin = plugin;
|
|
36275
|
-
await Promise.race([
|
|
36276
|
-
plugin.activate(makeCtx(data.pluginId, data.manifest)),
|
|
36277
|
-
new Promise(function (_, reject) { setTimeout(function () { reject(new Error('Activation timeout (5000ms)')); }, 5000); })
|
|
36278
|
-
]);
|
|
36279
|
-
post({ type: 'activated', id: data.id });
|
|
36280
|
-
} catch (error) {
|
|
36281
|
-
post({ type: 'activate-error', id: data.id, error: error && error.message ? error.message : String(error) });
|
|
35457
|
+
if (data.type !== 'load') return;
|
|
35458
|
+
targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
|
|
35459
|
+
try {
|
|
35460
|
+
const blob = new Blob([data.source], { type: 'text/javascript' });
|
|
35461
|
+
const url = URL.createObjectURL(blob);
|
|
35462
|
+
let mod;
|
|
35463
|
+
try { mod = await import(url); } finally { URL.revokeObjectURL(url); }
|
|
35464
|
+
const plugin = mod.default || mod;
|
|
35465
|
+
if (typeof plugin.activate !== 'function') throw new Error('missing activate function');
|
|
35466
|
+
activePlugin = plugin;
|
|
35467
|
+
await Promise.race([
|
|
35468
|
+
plugin.activate(makeCtx(data.pluginId, data.manifest)),
|
|
35469
|
+
new Promise(function (_, reject) { setTimeout(function () { reject(new Error('Activation timeout (5000ms)')); }, 5000); })
|
|
35470
|
+
]);
|
|
35471
|
+
post({ type: 'activated', id: data.id });
|
|
35472
|
+
} catch (error) {
|
|
35473
|
+
post({ type: 'activate-error', id: data.id, error: error && error.message ? error.message : String(error) });
|
|
35474
|
+
}
|
|
35475
|
+
});
|
|
35476
|
+
post({ type: 'ready' });
|
|
35477
|
+
</script>
|
|
35478
|
+
</body></html>`;
|
|
35479
|
+
|
|
35480
|
+
// server/utils/plugin-view-bridge.ts
|
|
35481
|
+
var PLUGIN_VIEW_CSP = "sandbox allow-scripts allow-forms allow-downloads; frame-ancestors 'self'; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src 'self' 'unsafe-inline' data: blob: https:; style-src-attr 'unsafe-inline'; connect-src 'self' blob: data: https:;";
|
|
35482
|
+
var PLUGIN_VIEW_BRIDGE_JS = `(function () {
|
|
35483
|
+
'use strict';
|
|
35484
|
+
const CHANNEL = 'ol-plugin-view';
|
|
35485
|
+
let targetOrigin = '*';
|
|
35486
|
+
const pending = new Map();
|
|
35487
|
+
const initCallbacks = [];
|
|
35488
|
+
const contextUpdateCallbacks = [];
|
|
35489
|
+
let currentContext = null;
|
|
35490
|
+
|
|
35491
|
+
function post(msg) {
|
|
35492
|
+
if (window.parent && window.parent !== window) {
|
|
35493
|
+
window.parent.postMessage(Object.assign({ channel: CHANNEL }, msg), targetOrigin);
|
|
35494
|
+
}
|
|
35495
|
+
}
|
|
35496
|
+
|
|
35497
|
+
function call(op, args) {
|
|
35498
|
+
const callId = Math.random().toString(16).slice(2) + Date.now().toString(16);
|
|
35499
|
+
return new Promise(function (resolve, reject) {
|
|
35500
|
+
pending.set(callId, { resolve: resolve, reject: reject });
|
|
35501
|
+
post({ type: 'call', callId: callId, op: op, args: args || [] });
|
|
35502
|
+
});
|
|
35503
|
+
}
|
|
35504
|
+
|
|
35505
|
+
window.addEventListener('message', function (event) {
|
|
35506
|
+
const data = event.data;
|
|
35507
|
+
if (!data || data.channel !== CHANNEL) return;
|
|
35508
|
+
if (event.source !== window.parent) return;
|
|
35509
|
+
|
|
35510
|
+
if (data.type === 'init') {
|
|
35511
|
+
currentContext = data.payload || {};
|
|
35512
|
+
targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
|
|
35513
|
+
for (let i = 0; i < initCallbacks.length; i++) {
|
|
35514
|
+
try {
|
|
35515
|
+
initCallbacks[i](currentContext);
|
|
35516
|
+
} catch (e) {
|
|
35517
|
+
console.error('[OpenLearnPluginBridge] init callback error:', e);
|
|
35518
|
+
}
|
|
35519
|
+
}
|
|
35520
|
+
return;
|
|
35521
|
+
}
|
|
35522
|
+
|
|
35523
|
+
if (data.type === 'context-update') {
|
|
35524
|
+
currentContext = Object.assign({}, currentContext, data.payload || {});
|
|
35525
|
+
for (let i = 0; i < contextUpdateCallbacks.length; i++) {
|
|
35526
|
+
try {
|
|
35527
|
+
contextUpdateCallbacks[i](currentContext);
|
|
35528
|
+
} catch (e) {
|
|
35529
|
+
console.error('[OpenLearnPluginBridge] contextUpdate callback error:', e);
|
|
35530
|
+
}
|
|
35531
|
+
}
|
|
35532
|
+
return;
|
|
35533
|
+
}
|
|
35534
|
+
|
|
35535
|
+
if (data.type === 'reply') {
|
|
35536
|
+
const waiter = pending.get(data.callId);
|
|
35537
|
+
if (!waiter) return;
|
|
35538
|
+
pending.delete(data.callId);
|
|
35539
|
+
if (data.ok) {
|
|
35540
|
+
waiter.resolve(data.value);
|
|
35541
|
+
} else {
|
|
35542
|
+
waiter.reject(new Error(data.error || 'bridge call failed'));
|
|
35543
|
+
}
|
|
35544
|
+
return;
|
|
35545
|
+
}
|
|
35546
|
+
});
|
|
35547
|
+
|
|
35548
|
+
const bridge = {
|
|
35549
|
+
CHANNEL: CHANNEL,
|
|
35550
|
+
onInit: function (callback) {
|
|
35551
|
+
if (typeof callback === 'function') {
|
|
35552
|
+
if (currentContext) {
|
|
35553
|
+
try {
|
|
35554
|
+
callback(currentContext);
|
|
35555
|
+
} catch (e) {
|
|
35556
|
+
console.error('[OpenLearnPluginBridge] onInit callback error:', e);
|
|
35557
|
+
}
|
|
35558
|
+
}
|
|
35559
|
+
initCallbacks.push(callback);
|
|
35560
|
+
}
|
|
35561
|
+
return bridge;
|
|
35562
|
+
},
|
|
35563
|
+
onContextUpdate: function (callback) {
|
|
35564
|
+
if (typeof callback === 'function') {
|
|
35565
|
+
contextUpdateCallbacks.push(callback);
|
|
35566
|
+
}
|
|
35567
|
+
return bridge;
|
|
35568
|
+
},
|
|
35569
|
+
getContext: function () {
|
|
35570
|
+
return currentContext;
|
|
35571
|
+
},
|
|
35572
|
+
ui: {
|
|
35573
|
+
toast: function (message, type) {
|
|
35574
|
+
return call('ui.toast', [message, type || 'info']);
|
|
35575
|
+
},
|
|
35576
|
+
showToast: function (message, type) {
|
|
35577
|
+
return call('ui.toast', [message, type || 'info']);
|
|
35578
|
+
},
|
|
35579
|
+
resize: function (height) {
|
|
35580
|
+
post({ type: 'resize', height: Number(height) || 0 });
|
|
35581
|
+
},
|
|
35582
|
+
},
|
|
35583
|
+
invokeCommand: function (type, payload) {
|
|
35584
|
+
return call('command.invoke', [type, payload]);
|
|
35585
|
+
},
|
|
35586
|
+
api: {
|
|
35587
|
+
get: function (path) {
|
|
35588
|
+
return call('api.get', [path]);
|
|
35589
|
+
},
|
|
35590
|
+
post: function (path, body) {
|
|
35591
|
+
return call('api.post', [path, body]);
|
|
35592
|
+
},
|
|
35593
|
+
del: function (path) {
|
|
35594
|
+
return call('api.del', [path]);
|
|
35595
|
+
},
|
|
35596
|
+
},
|
|
35597
|
+
storage: {
|
|
35598
|
+
get: function (key) {
|
|
35599
|
+
return call('storage.get', [key]);
|
|
35600
|
+
},
|
|
35601
|
+
set: function (key, value) {
|
|
35602
|
+
return call('storage.set', [key, value]);
|
|
35603
|
+
},
|
|
35604
|
+
delete: function (key) {
|
|
35605
|
+
return call('storage.delete', [key]);
|
|
35606
|
+
},
|
|
35607
|
+
},
|
|
35608
|
+
};
|
|
35609
|
+
|
|
35610
|
+
window.OpenLearnPluginBridge = bridge;
|
|
35611
|
+
// \u901A\u77E5\u5BBF\u4E3B\u89C6\u56FE\u6C99\u7BB1 iframe \u5DF2\u5C31\u7EEA
|
|
35612
|
+
post({ type: 'ready' });
|
|
35613
|
+
})();
|
|
35614
|
+
`;
|
|
35615
|
+
|
|
35616
|
+
// packages/core/bootstrap/types/index.ts
|
|
35617
|
+
var PlatformBootstrapError = class extends Error {
|
|
35618
|
+
constructor(message, stage, cause) {
|
|
35619
|
+
super(`[PlatformBootstrapError${stage ? `:${stage}` : ""}] ${message}`);
|
|
35620
|
+
this.stage = stage;
|
|
35621
|
+
this.cause = cause;
|
|
35622
|
+
this.name = "PlatformBootstrapError";
|
|
35623
|
+
}
|
|
35624
|
+
};
|
|
35625
|
+
var ConfigurationError = class extends PlatformBootstrapError {
|
|
35626
|
+
constructor(message, cause) {
|
|
35627
|
+
super(message, "Configuring" /* Configuring */, cause);
|
|
35628
|
+
this.name = "ConfigurationError";
|
|
35629
|
+
}
|
|
35630
|
+
};
|
|
35631
|
+
var StartupTimeoutError = class extends PlatformBootstrapError {
|
|
35632
|
+
constructor(message, stage, cause) {
|
|
35633
|
+
super(message, stage, cause);
|
|
35634
|
+
this.name = "StartupTimeoutError";
|
|
35635
|
+
}
|
|
35636
|
+
};
|
|
35637
|
+
var PLATFORM_VERSION2 = PLATFORM_VERSION;
|
|
35638
|
+
var BOOTSTRAP_STAGE_NAMES = Object.freeze([
|
|
35639
|
+
"Created" /* Created */,
|
|
35640
|
+
"Configuring" /* Configuring */,
|
|
35641
|
+
"Registering" /* Registering */,
|
|
35642
|
+
"Initializing" /* Initializing */,
|
|
35643
|
+
"Activating" /* Activating */,
|
|
35644
|
+
"Ready" /* Ready */,
|
|
35645
|
+
"ShuttingDown" /* ShuttingDown */,
|
|
35646
|
+
"Disposed" /* Disposed */
|
|
35647
|
+
]);
|
|
35648
|
+
var DEFAULT_BOOTSTRAP_CONFIG = Object.freeze({
|
|
35649
|
+
environment: "development",
|
|
35650
|
+
mode: "standalone",
|
|
35651
|
+
port: 9e3,
|
|
35652
|
+
debug: false,
|
|
35653
|
+
pluginsDir: "./plugins",
|
|
35654
|
+
dbPath: "./packages/core/db/educational_os.db"
|
|
35655
|
+
});
|
|
35656
|
+
|
|
35657
|
+
// packages/core/bootstrap/pipeline/pipeline-executor.ts
|
|
35658
|
+
var PipelineExecutor = class {
|
|
35659
|
+
constructor() {
|
|
35660
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
35661
|
+
}
|
|
35662
|
+
addListener(listener) {
|
|
35663
|
+
this.listeners.add(listener);
|
|
35664
|
+
return () => {
|
|
35665
|
+
this.listeners.delete(listener);
|
|
35666
|
+
};
|
|
35667
|
+
}
|
|
35668
|
+
emit(event) {
|
|
35669
|
+
for (const listener of Array.from(this.listeners)) {
|
|
35670
|
+
try {
|
|
35671
|
+
listener(event);
|
|
35672
|
+
} catch (err) {
|
|
35673
|
+
console.error("[PipelineExecutor] Listener error:", err);
|
|
35674
|
+
}
|
|
35675
|
+
}
|
|
35676
|
+
}
|
|
35677
|
+
async execute(stages, context) {
|
|
35678
|
+
const startTime = Date.now();
|
|
35679
|
+
const results = [];
|
|
35680
|
+
const executedStages = [];
|
|
35681
|
+
this.emit({ type: "PipelineStarted", timestamp: startTime });
|
|
35682
|
+
console.log("[Platform] Starting Bootstrap Pipeline execution...");
|
|
35683
|
+
for (const stage of stages) {
|
|
35684
|
+
if (context.isCancelled) {
|
|
35685
|
+
console.warn(`[Platform] Pipeline cancelled before stage ${stage.name}`);
|
|
35686
|
+
const totalDurationMs2 = Date.now() - startTime;
|
|
35687
|
+
return {
|
|
35688
|
+
totalDurationMs: totalDurationMs2,
|
|
35689
|
+
status: "Aborted",
|
|
35690
|
+
stageResults: Object.freeze(results)
|
|
35691
|
+
};
|
|
35692
|
+
}
|
|
35693
|
+
console.log(`[Platform] Entering ${stage.name}`);
|
|
35694
|
+
this.emit({
|
|
35695
|
+
type: "StageStarted",
|
|
35696
|
+
timestamp: Date.now(),
|
|
35697
|
+
stageId: stage.id,
|
|
35698
|
+
stageName: String(stage.name)
|
|
35699
|
+
});
|
|
35700
|
+
const stageStart = Date.now();
|
|
35701
|
+
try {
|
|
35702
|
+
if (stage.timeoutMs && stage.timeoutMs > 0) {
|
|
35703
|
+
await this.executeWithTimeout(stage, context, stage.timeoutMs);
|
|
35704
|
+
} else {
|
|
35705
|
+
await stage.execute(context);
|
|
35706
|
+
}
|
|
35707
|
+
const durationMs = Date.now() - stageStart;
|
|
35708
|
+
console.log(`[Platform] Completed ${stage.name} (${durationMs} ms)`);
|
|
35709
|
+
results.push({
|
|
35710
|
+
stageId: stage.id,
|
|
35711
|
+
stageName: String(stage.name),
|
|
35712
|
+
durationMs,
|
|
35713
|
+
status: "Success"
|
|
35714
|
+
});
|
|
35715
|
+
executedStages.push(stage);
|
|
35716
|
+
this.emit({
|
|
35717
|
+
type: "StageCompleted",
|
|
35718
|
+
timestamp: Date.now(),
|
|
35719
|
+
stageId: stage.id,
|
|
35720
|
+
stageName: String(stage.name),
|
|
35721
|
+
durationMs
|
|
35722
|
+
});
|
|
35723
|
+
} catch (err) {
|
|
35724
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
35725
|
+
const durationMs = Date.now() - stageStart;
|
|
35726
|
+
console.error(`[Platform] Failed ${stage.name} (${durationMs} ms): ${error.message}`);
|
|
35727
|
+
results.push({
|
|
35728
|
+
stageId: stage.id,
|
|
35729
|
+
stageName: String(stage.name),
|
|
35730
|
+
durationMs,
|
|
35731
|
+
status: "Failed",
|
|
35732
|
+
error
|
|
35733
|
+
});
|
|
35734
|
+
this.emit({
|
|
35735
|
+
type: "StageFailed",
|
|
35736
|
+
timestamp: Date.now(),
|
|
35737
|
+
stageId: stage.id,
|
|
35738
|
+
stageName: String(stage.name),
|
|
35739
|
+
durationMs,
|
|
35740
|
+
error
|
|
35741
|
+
});
|
|
35742
|
+
await this.rollbackExecutedStages(executedStages, context);
|
|
35743
|
+
const totalDurationMs2 = Date.now() - startTime;
|
|
35744
|
+
const pipelineResult = {
|
|
35745
|
+
totalDurationMs: totalDurationMs2,
|
|
35746
|
+
status: "Failed",
|
|
35747
|
+
stageResults: Object.freeze(results),
|
|
35748
|
+
failedStage: stage.id,
|
|
35749
|
+
error
|
|
35750
|
+
};
|
|
35751
|
+
this.emit({
|
|
35752
|
+
type: "PipelineCompleted",
|
|
35753
|
+
timestamp: Date.now(),
|
|
35754
|
+
durationMs: totalDurationMs2,
|
|
35755
|
+
error
|
|
35756
|
+
});
|
|
35757
|
+
return pipelineResult;
|
|
35758
|
+
}
|
|
35759
|
+
}
|
|
35760
|
+
const totalDurationMs = Date.now() - startTime;
|
|
35761
|
+
console.log(`[Platform] Ready. Total startup duration: ${totalDurationMs} ms.`);
|
|
35762
|
+
const finalResult = {
|
|
35763
|
+
totalDurationMs,
|
|
35764
|
+
status: "Success",
|
|
35765
|
+
stageResults: Object.freeze(results)
|
|
35766
|
+
};
|
|
35767
|
+
this.emit({
|
|
35768
|
+
type: "PipelineCompleted",
|
|
35769
|
+
timestamp: Date.now(),
|
|
35770
|
+
durationMs: totalDurationMs
|
|
35771
|
+
});
|
|
35772
|
+
return finalResult;
|
|
35773
|
+
}
|
|
35774
|
+
async executeWithTimeout(stage, context, timeoutMs) {
|
|
35775
|
+
let timer;
|
|
35776
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
35777
|
+
timer = setTimeout(() => {
|
|
35778
|
+
reject(new StartupTimeoutError(`Stage execution timed out after ${timeoutMs} ms`));
|
|
35779
|
+
}, timeoutMs);
|
|
35780
|
+
});
|
|
35781
|
+
try {
|
|
35782
|
+
await Promise.race([stage.execute(context), timeoutPromise]);
|
|
35783
|
+
} finally {
|
|
35784
|
+
if (timer) {
|
|
35785
|
+
clearTimeout(timer);
|
|
35786
|
+
}
|
|
35787
|
+
}
|
|
35788
|
+
}
|
|
35789
|
+
async rollbackExecutedStages(executedStages, context) {
|
|
35790
|
+
console.log("[Platform] Triggering stage rollbacks...");
|
|
35791
|
+
for (let i = executedStages.length - 1; i >= 0; i--) {
|
|
35792
|
+
const stage = executedStages[i];
|
|
35793
|
+
if (stage.rollback) {
|
|
35794
|
+
try {
|
|
35795
|
+
console.log(`[Platform] Rolling back ${stage.name}`);
|
|
35796
|
+
await stage.rollback(context);
|
|
35797
|
+
} catch (rbErr) {
|
|
35798
|
+
console.error(`[Platform] Error during rollback of ${stage.name}:`, rbErr);
|
|
35799
|
+
}
|
|
35800
|
+
}
|
|
35801
|
+
}
|
|
35802
|
+
}
|
|
35803
|
+
};
|
|
35804
|
+
|
|
35805
|
+
// packages/core/bootstrap/pipeline/stages/standard-stages.ts
|
|
35806
|
+
var StartupStageImpl = class {
|
|
35807
|
+
constructor() {
|
|
35808
|
+
this.id = "stage_startup";
|
|
35809
|
+
this.name = "Created" /* Created */;
|
|
35810
|
+
this.description = "Environment & configuration validation";
|
|
35811
|
+
}
|
|
35812
|
+
async execute(context) {
|
|
35813
|
+
context.setStage("Configuring" /* Configuring */);
|
|
35814
|
+
}
|
|
35815
|
+
};
|
|
35816
|
+
var RegistrationStageImpl = class {
|
|
35817
|
+
constructor() {
|
|
35818
|
+
this.id = "stage_registration";
|
|
35819
|
+
this.name = "Registering" /* Registering */;
|
|
35820
|
+
this.description = "Core Service Contracts & DI Tokens registration";
|
|
35821
|
+
}
|
|
35822
|
+
async execute(context) {
|
|
35823
|
+
context.setStage("Registering" /* Registering */);
|
|
35824
|
+
}
|
|
35825
|
+
};
|
|
35826
|
+
var InitializationStageImpl = class {
|
|
35827
|
+
constructor() {
|
|
35828
|
+
this.id = "stage_initialization";
|
|
35829
|
+
this.name = "Initializing" /* Initializing */;
|
|
35830
|
+
this.description = "Subsystem Kernels initialization";
|
|
35831
|
+
}
|
|
35832
|
+
async execute(context) {
|
|
35833
|
+
context.setStage("Initializing" /* Initializing */);
|
|
35834
|
+
}
|
|
35835
|
+
};
|
|
35836
|
+
var ActivationStageImpl = class {
|
|
35837
|
+
constructor() {
|
|
35838
|
+
this.id = "stage_activation";
|
|
35839
|
+
this.name = "Activating" /* Activating */;
|
|
35840
|
+
this.description = "Plugin Host discovery & ESM plugins activation";
|
|
35841
|
+
}
|
|
35842
|
+
async execute(context) {
|
|
35843
|
+
context.setStage("Activating" /* Activating */);
|
|
35844
|
+
}
|
|
35845
|
+
};
|
|
35846
|
+
var ReadyStageImpl = class {
|
|
35847
|
+
constructor() {
|
|
35848
|
+
this.id = "stage_ready";
|
|
35849
|
+
this.name = "Ready" /* Ready */;
|
|
35850
|
+
this.description = "HTTP & Socket.IO server readiness";
|
|
35851
|
+
}
|
|
35852
|
+
async execute(context) {
|
|
35853
|
+
context.setStage("Ready" /* Ready */);
|
|
35854
|
+
}
|
|
35855
|
+
};
|
|
35856
|
+
|
|
35857
|
+
// packages/core/bootstrap/pipeline/bootstrap-pipeline.ts
|
|
35858
|
+
var BootstrapPipeline = class {
|
|
35859
|
+
constructor(stages) {
|
|
35860
|
+
this._stages = [];
|
|
35861
|
+
this.executor = new PipelineExecutor();
|
|
35862
|
+
if (stages && stages.length > 0) {
|
|
35863
|
+
this._stages = [...stages];
|
|
35864
|
+
} else {
|
|
35865
|
+
this._stages = [
|
|
35866
|
+
new StartupStageImpl(),
|
|
35867
|
+
new RegistrationStageImpl(),
|
|
35868
|
+
new InitializationStageImpl(),
|
|
35869
|
+
new ActivationStageImpl(),
|
|
35870
|
+
new ReadyStageImpl()
|
|
35871
|
+
];
|
|
35872
|
+
}
|
|
35873
|
+
}
|
|
35874
|
+
get stages() {
|
|
35875
|
+
return Object.freeze(this._stages);
|
|
35876
|
+
}
|
|
35877
|
+
addStage(stage) {
|
|
35878
|
+
this._stages.push(stage);
|
|
35879
|
+
return this;
|
|
36282
35880
|
}
|
|
36283
|
-
|
|
36284
|
-
|
|
36285
|
-
|
|
36286
|
-
|
|
36287
|
-
|
|
36288
|
-
|
|
36289
|
-
|
|
36290
|
-
var PLUGIN_VIEW_BRIDGE_JS = `(function () {
|
|
36291
|
-
'use strict';
|
|
36292
|
-
const CHANNEL = 'ol-plugin-view';
|
|
36293
|
-
let targetOrigin = '*';
|
|
36294
|
-
const pending = new Map();
|
|
36295
|
-
const initCallbacks = [];
|
|
36296
|
-
const contextUpdateCallbacks = [];
|
|
36297
|
-
let currentContext = null;
|
|
36298
|
-
|
|
36299
|
-
function post(msg) {
|
|
36300
|
-
if (window.parent && window.parent !== window) {
|
|
36301
|
-
window.parent.postMessage(Object.assign({ channel: CHANNEL }, msg), targetOrigin);
|
|
35881
|
+
addListener(listener) {
|
|
35882
|
+
return this.executor.addListener(listener);
|
|
35883
|
+
}
|
|
35884
|
+
async run(context) {
|
|
35885
|
+
const result = await this.execute(context);
|
|
35886
|
+
if (result.status === "Failed") {
|
|
35887
|
+
throw result.error || new Error(`Bootstrap Pipeline failed at stage: ${result.failedStage}`);
|
|
36302
35888
|
}
|
|
36303
35889
|
}
|
|
36304
|
-
|
|
36305
|
-
|
|
36306
|
-
const callId = Math.random().toString(16).slice(2) + Date.now().toString(16);
|
|
36307
|
-
return new Promise(function (resolve, reject) {
|
|
36308
|
-
pending.set(callId, { resolve: resolve, reject: reject });
|
|
36309
|
-
post({ type: 'call', callId: callId, op: op, args: args || [] });
|
|
36310
|
-
});
|
|
35890
|
+
async execute(context) {
|
|
35891
|
+
return this.executor.execute(this._stages, context);
|
|
36311
35892
|
}
|
|
35893
|
+
};
|
|
36312
35894
|
|
|
36313
|
-
|
|
36314
|
-
|
|
36315
|
-
|
|
36316
|
-
|
|
36317
|
-
|
|
36318
|
-
if (
|
|
36319
|
-
|
|
36320
|
-
|
|
36321
|
-
|
|
36322
|
-
|
|
36323
|
-
|
|
36324
|
-
|
|
36325
|
-
|
|
35895
|
+
// packages/core/bootstrap/builder/builder-validation-engine.ts
|
|
35896
|
+
var BuilderValidationEngine = class {
|
|
35897
|
+
static validate(config, environment, stages) {
|
|
35898
|
+
const errors = [];
|
|
35899
|
+
const warnings = [];
|
|
35900
|
+
if (!["development", "production", "test"].includes(environment)) {
|
|
35901
|
+
errors.push({
|
|
35902
|
+
code: "INVALID_ENVIRONMENT",
|
|
35903
|
+
message: `Environment must be one of 'development', 'production', 'test'. Received: '${environment}'`,
|
|
35904
|
+
field: "environment"
|
|
35905
|
+
});
|
|
35906
|
+
}
|
|
35907
|
+
if (!config.port || config.port <= 0 || config.port > 65535) {
|
|
35908
|
+
errors.push({
|
|
35909
|
+
code: "INVALID_PORT",
|
|
35910
|
+
message: `Port must be a valid number between 1 and 65535. Received: ${config.port}`,
|
|
35911
|
+
field: "port"
|
|
35912
|
+
});
|
|
35913
|
+
}
|
|
35914
|
+
if (!stages || stages.length === 0) {
|
|
35915
|
+
errors.push({
|
|
35916
|
+
code: "MISSING_STAGES",
|
|
35917
|
+
message: "Bootstrap pipeline contains no registered stages.",
|
|
35918
|
+
field: "stages"
|
|
35919
|
+
});
|
|
35920
|
+
} else {
|
|
35921
|
+
const stageIds = /* @__PURE__ */ new Set();
|
|
35922
|
+
for (const stage of stages) {
|
|
35923
|
+
if (!stage.id) {
|
|
35924
|
+
errors.push({
|
|
35925
|
+
code: "INVALID_STAGE",
|
|
35926
|
+
message: `Stage '${stage.name}' has no unique ID.`
|
|
35927
|
+
});
|
|
35928
|
+
} else if (stageIds.has(stage.id)) {
|
|
35929
|
+
errors.push({
|
|
35930
|
+
code: "DUPLICATE_STAGE",
|
|
35931
|
+
message: `Duplicate stage registration detected for stage ID '${stage.id}'.`,
|
|
35932
|
+
field: "stage.id"
|
|
35933
|
+
});
|
|
35934
|
+
} else {
|
|
35935
|
+
stageIds.add(stage.id);
|
|
36326
35936
|
}
|
|
36327
35937
|
}
|
|
36328
|
-
return;
|
|
36329
35938
|
}
|
|
35939
|
+
return {
|
|
35940
|
+
isValid: errors.length === 0,
|
|
35941
|
+
errors: Object.freeze(errors),
|
|
35942
|
+
warnings: Object.freeze(warnings)
|
|
35943
|
+
};
|
|
35944
|
+
}
|
|
35945
|
+
};
|
|
36330
35946
|
|
|
36331
|
-
|
|
36332
|
-
|
|
36333
|
-
|
|
36334
|
-
|
|
36335
|
-
|
|
36336
|
-
|
|
36337
|
-
|
|
35947
|
+
// packages/core/bootstrap/builder/platform-builder.ts
|
|
35948
|
+
var DefaultPlatformLogger = class {
|
|
35949
|
+
info(message, ...args) {
|
|
35950
|
+
console.log(`[PlatformBuilder:INFO] ${message}`, ...args);
|
|
35951
|
+
}
|
|
35952
|
+
warn(message, ...args) {
|
|
35953
|
+
console.warn(`[PlatformBuilder:WARN] ${message}`, ...args);
|
|
35954
|
+
}
|
|
35955
|
+
error(message, ...args) {
|
|
35956
|
+
console.error(`[PlatformBuilder:ERROR] ${message}`, ...args);
|
|
35957
|
+
}
|
|
35958
|
+
debug(message, ...args) {
|
|
35959
|
+
console.debug(`[PlatformBuilder:DEBUG] ${message}`, ...args);
|
|
35960
|
+
}
|
|
35961
|
+
};
|
|
35962
|
+
var PlatformBuilder = class _PlatformBuilder {
|
|
35963
|
+
constructor(options) {
|
|
35964
|
+
this._state = "Created";
|
|
35965
|
+
this._config = { ...DEFAULT_BOOTSTRAP_CONFIG };
|
|
35966
|
+
this._logger = new DefaultPlatformLogger();
|
|
35967
|
+
this._environment = "development";
|
|
35968
|
+
this._metadata = /* @__PURE__ */ new Map();
|
|
35969
|
+
this._services = /* @__PURE__ */ new Map();
|
|
35970
|
+
this._capabilities = /* @__PURE__ */ new Map();
|
|
35971
|
+
this._extensions = /* @__PURE__ */ new Map();
|
|
35972
|
+
this._pipeline = new BootstrapPipeline();
|
|
35973
|
+
this._state = "Created";
|
|
35974
|
+
this._logger.info("Builder Started");
|
|
35975
|
+
if (options) {
|
|
35976
|
+
if (options.config) this.withConfiguration(options.config);
|
|
35977
|
+
if (options.logger) this.withLogger(options.logger);
|
|
35978
|
+
if (options.environment) this.withEnvironment(options.environment);
|
|
35979
|
+
if (options.metadata) {
|
|
35980
|
+
for (const [k, v] of Object.entries(options.metadata)) {
|
|
35981
|
+
this.withMetadata(k, v);
|
|
36338
35982
|
}
|
|
36339
35983
|
}
|
|
36340
|
-
return;
|
|
36341
35984
|
}
|
|
36342
|
-
|
|
36343
|
-
|
|
36344
|
-
|
|
36345
|
-
|
|
36346
|
-
|
|
36347
|
-
|
|
36348
|
-
|
|
36349
|
-
|
|
36350
|
-
|
|
36351
|
-
|
|
36352
|
-
return;
|
|
35985
|
+
}
|
|
35986
|
+
static create(options) {
|
|
35987
|
+
return new _PlatformBuilder(options);
|
|
35988
|
+
}
|
|
35989
|
+
get state() {
|
|
35990
|
+
return this._state;
|
|
35991
|
+
}
|
|
35992
|
+
transitionTo(newState) {
|
|
35993
|
+
if (this._state === "Disposed") {
|
|
35994
|
+
throw new Error("PlatformBuilder has been disposed and cannot accept further transitions.");
|
|
36353
35995
|
}
|
|
36354
|
-
|
|
36355
|
-
|
|
36356
|
-
|
|
36357
|
-
|
|
36358
|
-
|
|
36359
|
-
|
|
36360
|
-
|
|
36361
|
-
|
|
36362
|
-
|
|
36363
|
-
|
|
36364
|
-
|
|
36365
|
-
|
|
36366
|
-
|
|
36367
|
-
|
|
36368
|
-
|
|
36369
|
-
|
|
36370
|
-
|
|
36371
|
-
|
|
36372
|
-
|
|
36373
|
-
|
|
36374
|
-
|
|
36375
|
-
|
|
36376
|
-
}
|
|
36377
|
-
|
|
36378
|
-
|
|
36379
|
-
|
|
36380
|
-
|
|
36381
|
-
|
|
36382
|
-
|
|
35996
|
+
if (this._state === "Built" && newState !== "Disposed") {
|
|
35997
|
+
throw new Error(`Invalid state transition from Built to ${newState}. Builder is already built.`);
|
|
35998
|
+
}
|
|
35999
|
+
this._state = newState;
|
|
36000
|
+
}
|
|
36001
|
+
withConfiguration(config) {
|
|
36002
|
+
this.transitionTo("Configuring");
|
|
36003
|
+
this._config = { ...this._config, ...config };
|
|
36004
|
+
if (config.environment) {
|
|
36005
|
+
this._environment = config.environment;
|
|
36006
|
+
}
|
|
36007
|
+
this._logger.info("Configuration Loaded");
|
|
36008
|
+
return this;
|
|
36009
|
+
}
|
|
36010
|
+
withLogger(logger2) {
|
|
36011
|
+
this.transitionTo("Configuring");
|
|
36012
|
+
this._logger = logger2;
|
|
36013
|
+
return this;
|
|
36014
|
+
}
|
|
36015
|
+
withEnvironment(env) {
|
|
36016
|
+
this.transitionTo("Configuring");
|
|
36017
|
+
this._environment = env;
|
|
36018
|
+
this._config = { ...this._config, environment: env };
|
|
36019
|
+
return this;
|
|
36020
|
+
}
|
|
36021
|
+
withMetadata(key, value) {
|
|
36022
|
+
this.transitionTo("Configuring");
|
|
36023
|
+
this._metadata.set(key, value);
|
|
36024
|
+
return this;
|
|
36025
|
+
}
|
|
36026
|
+
addBootstrapStage(stage) {
|
|
36027
|
+
this.transitionTo("Configuring");
|
|
36028
|
+
this._pipeline.addStage(stage);
|
|
36029
|
+
return this;
|
|
36030
|
+
}
|
|
36031
|
+
addService(serviceId, instance) {
|
|
36032
|
+
this.transitionTo("Configuring");
|
|
36033
|
+
this._services.set(serviceId, instance);
|
|
36034
|
+
return this;
|
|
36035
|
+
}
|
|
36036
|
+
addCapability(capabilityId, descriptor) {
|
|
36037
|
+
this.transitionTo("Configuring");
|
|
36038
|
+
this._capabilities.set(capabilityId, descriptor);
|
|
36039
|
+
return this;
|
|
36040
|
+
}
|
|
36041
|
+
addExtension(extensionId, extensionSpec) {
|
|
36042
|
+
this.transitionTo("Configuring");
|
|
36043
|
+
this._extensions.set(extensionId, extensionSpec);
|
|
36044
|
+
return this;
|
|
36045
|
+
}
|
|
36046
|
+
buildResult() {
|
|
36047
|
+
const buildStart = Date.now();
|
|
36048
|
+
this.transitionTo("Validating");
|
|
36049
|
+
this._logger.info("Validation Started");
|
|
36050
|
+
const validation = BuilderValidationEngine.validate(this._config, this._environment, this._pipeline.stages);
|
|
36051
|
+
this._logger.info("Validation Completed");
|
|
36052
|
+
if (!validation.isValid) {
|
|
36053
|
+
this._logger.error(`Validation Failed with ${validation.errors.length} errors.`);
|
|
36054
|
+
throw new ConfigurationError(
|
|
36055
|
+
`PlatformBuilder validation failed: ${validation.errors.map((e) => e.message).join("; ")}`
|
|
36056
|
+
);
|
|
36057
|
+
}
|
|
36058
|
+
this.transitionTo("Building");
|
|
36059
|
+
this._logger.info("Build Started");
|
|
36060
|
+
const platformContext = {
|
|
36061
|
+
platformId: `plt_${globalThis.crypto.randomUUID()}`,
|
|
36062
|
+
version: PLATFORM_VERSION2,
|
|
36063
|
+
namespace: "openlearn.core",
|
|
36064
|
+
mode: this._config.mode,
|
|
36065
|
+
environment: {
|
|
36066
|
+
type: this._environment,
|
|
36067
|
+
isDevelopment: this._environment === "development",
|
|
36068
|
+
isProduction: this._environment === "production",
|
|
36069
|
+
isTest: this._environment === "test",
|
|
36070
|
+
isPluginSandbox: false
|
|
36383
36071
|
},
|
|
36384
|
-
|
|
36385
|
-
|
|
36072
|
+
config: this._config,
|
|
36073
|
+
featureFlags: {
|
|
36074
|
+
isEnabled: () => true,
|
|
36075
|
+
getFlag: (_, def) => def,
|
|
36076
|
+
getAllFlags: () => ({})
|
|
36386
36077
|
},
|
|
36387
|
-
|
|
36388
|
-
|
|
36078
|
+
runtimeMetadata: {
|
|
36079
|
+
os: process.platform,
|
|
36080
|
+
nodeVersion: process.version,
|
|
36081
|
+
arch: process.arch,
|
|
36082
|
+
processId: process.pid,
|
|
36083
|
+
memoryUsage: process.memoryUsage(),
|
|
36084
|
+
buildVersion: PLATFORM_VERSION2
|
|
36389
36085
|
},
|
|
36390
|
-
|
|
36391
|
-
|
|
36392
|
-
|
|
36393
|
-
|
|
36394
|
-
|
|
36395
|
-
get: function (path) {
|
|
36396
|
-
return call('api.get', [path]);
|
|
36086
|
+
bootstrapState: "Bootstrapping",
|
|
36087
|
+
logger: this._logger,
|
|
36088
|
+
diagnostics: {
|
|
36089
|
+
getMetrics: () => ({ uptime: 0 }),
|
|
36090
|
+
getSystemStatus: () => ({ isHealthy: true, activeServicesCount: this._services.size, uptimeSeconds: 0 })
|
|
36397
36091
|
},
|
|
36398
|
-
|
|
36399
|
-
|
|
36092
|
+
serviceLocator: {
|
|
36093
|
+
hasService: (id) => this._services.has(id),
|
|
36094
|
+
getService: (id) => this._services.get(id)
|
|
36400
36095
|
},
|
|
36401
|
-
|
|
36402
|
-
|
|
36096
|
+
capabilityCatalog: {
|
|
36097
|
+
hasCapability: (id) => this._capabilities.has(id),
|
|
36098
|
+
getCapability: (id) => this._capabilities.get(id)
|
|
36403
36099
|
},
|
|
36404
|
-
|
|
36405
|
-
|
|
36406
|
-
|
|
36407
|
-
return call('storage.get', [key]);
|
|
36100
|
+
extensionCatalog: {
|
|
36101
|
+
hasExtension: (id) => this._extensions.has(id),
|
|
36102
|
+
getExtension: (id) => this._extensions.get(id)
|
|
36408
36103
|
},
|
|
36409
|
-
|
|
36410
|
-
|
|
36104
|
+
eventDispatcher: {
|
|
36105
|
+
dispatch: async () => {
|
|
36106
|
+
}
|
|
36411
36107
|
},
|
|
36412
|
-
|
|
36413
|
-
|
|
36108
|
+
shutdownToken: {
|
|
36109
|
+
isRequested: false,
|
|
36110
|
+
onShutdown: () => {
|
|
36111
|
+
}
|
|
36112
|
+
}
|
|
36113
|
+
};
|
|
36114
|
+
const buildDurationMs = Date.now() - buildStart;
|
|
36115
|
+
this.transitionTo("Built");
|
|
36116
|
+
this._logger.info(`Build Completed in ${buildDurationMs} ms`);
|
|
36117
|
+
const result = {
|
|
36118
|
+
platformContext,
|
|
36119
|
+
pipeline: this._pipeline,
|
|
36120
|
+
buildDurationMs,
|
|
36121
|
+
validation,
|
|
36122
|
+
metadata: Object.fromEntries(this._metadata.entries()),
|
|
36123
|
+
builderVersion: PLATFORM_VERSION2
|
|
36124
|
+
};
|
|
36125
|
+
return result;
|
|
36126
|
+
}
|
|
36127
|
+
async build() {
|
|
36128
|
+
const res = this.buildResult();
|
|
36129
|
+
return {
|
|
36130
|
+
currentStage: "Created" /* Created */,
|
|
36131
|
+
context: res.platformContext,
|
|
36132
|
+
pipeline: res.pipeline,
|
|
36133
|
+
start: async () => {
|
|
36414
36134
|
},
|
|
36415
|
-
|
|
36416
|
-
|
|
36417
|
-
|
|
36418
|
-
|
|
36419
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
36422
|
-
|
|
36135
|
+
shutdown: async () => {
|
|
36136
|
+
},
|
|
36137
|
+
dispose: async () => {
|
|
36138
|
+
}
|
|
36139
|
+
};
|
|
36140
|
+
}
|
|
36141
|
+
async start() {
|
|
36142
|
+
const kernel = await this.build();
|
|
36143
|
+
await kernel.start();
|
|
36144
|
+
return kernel;
|
|
36145
|
+
}
|
|
36146
|
+
async shutdown() {
|
|
36147
|
+
this.transitionTo("Disposed");
|
|
36148
|
+
}
|
|
36149
|
+
dispose() {
|
|
36150
|
+
this._state = "Disposed";
|
|
36151
|
+
this._services.clear();
|
|
36152
|
+
this._capabilities.clear();
|
|
36153
|
+
this._extensions.clear();
|
|
36154
|
+
this._metadata.clear();
|
|
36155
|
+
}
|
|
36156
|
+
};
|
|
36423
36157
|
|
|
36424
36158
|
// packages/core/bootstrap/adapter/bootstrap-registration.ts
|
|
36425
36159
|
var BootstrapRegistration = class {
|
|
@@ -38406,9 +38140,12 @@ var generateStudentNumber = (db2) => {
|
|
|
38406
38140
|
return `S${nextSeq.toString().padStart(3, "0")}`;
|
|
38407
38141
|
};
|
|
38408
38142
|
|
|
38143
|
+
// server/services/ai-provider-resolver.ts
|
|
38144
|
+
init_ai_provider_resolver();
|
|
38145
|
+
|
|
38409
38146
|
// server/routes/os.ts
|
|
38410
38147
|
function registerOsRoutes(ctx) {
|
|
38411
|
-
const { app,
|
|
38148
|
+
const { app, runOpenAIAgentChat: runOpenAIAgentChat2, activityRegistry, aiLimiter } = ctx;
|
|
38412
38149
|
app.get("/api/ping", (_req, res) => {
|
|
38413
38150
|
res.json({ ok: true, ts: Date.now() });
|
|
38414
38151
|
});
|
|
@@ -38627,13 +38364,10 @@ function registerOsRoutes(ctx) {
|
|
|
38627
38364
|
}
|
|
38628
38365
|
let callerRole;
|
|
38629
38366
|
let userId;
|
|
38630
|
-
const
|
|
38631
|
-
if (
|
|
38632
|
-
|
|
38633
|
-
|
|
38634
|
-
callerRole = session.role;
|
|
38635
|
-
userId = session.userId;
|
|
38636
|
-
}
|
|
38367
|
+
const session = req.session || (getCookieToken(req) ? getValidSession(getCookieToken(req)) : null);
|
|
38368
|
+
if (session) {
|
|
38369
|
+
callerRole = session.role;
|
|
38370
|
+
userId = session.userId || session.username;
|
|
38637
38371
|
}
|
|
38638
38372
|
const convKey = `agent:${userId || "anonymous"}:${currentLessonId || "global"}`;
|
|
38639
38373
|
const MEMORY_TURNS = 20;
|
|
@@ -38643,19 +38377,17 @@ function registerOsRoutes(ctx) {
|
|
|
38643
38377
|
content: r.content
|
|
38644
38378
|
}));
|
|
38645
38379
|
let provider;
|
|
38646
|
-
|
|
38647
|
-
provider = kernelContainer.db
|
|
38648
|
-
|
|
38649
|
-
|
|
38650
|
-
|
|
38651
|
-
)
|
|
38652
|
-
}
|
|
38653
|
-
|
|
38654
|
-
if (!provider || !provider.api_key || !provider.api_key.trim()) {
|
|
38655
|
-
return res.status(400).json({
|
|
38380
|
+
try {
|
|
38381
|
+
provider = resolveAccessibleAiProvider(kernelContainer.db, {
|
|
38382
|
+
providerId,
|
|
38383
|
+
userId,
|
|
38384
|
+
lang
|
|
38385
|
+
});
|
|
38386
|
+
} catch (err) {
|
|
38387
|
+
return res.status(err.status || 400).json({
|
|
38656
38388
|
success: false,
|
|
38657
|
-
error: "NO_AI_PROVIDER",
|
|
38658
|
-
message:
|
|
38389
|
+
error: err.code || "NO_AI_PROVIDER",
|
|
38390
|
+
message: err.message
|
|
38659
38391
|
});
|
|
38660
38392
|
}
|
|
38661
38393
|
const actorId = getActorId(req);
|
|
@@ -42119,7 +41851,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
42119
41851
|
* 的按学科硬编码字符串拼接,与课题内容无关。此方法用真实的 LLM 调用取代它。
|
|
42120
41852
|
*/
|
|
42121
41853
|
async generateOutlineWithAi(params) {
|
|
42122
|
-
const { title, category, description, timeline, lang = "zh" } = params;
|
|
41854
|
+
const { title, category, description, timeline, lang = "zh", providerId, userId } = params;
|
|
42123
41855
|
const trimmedTitle = (title || "").trim();
|
|
42124
41856
|
if (!trimmedTitle) {
|
|
42125
41857
|
const err = new Error(lang === "zh" ? "\u8BFE\u7A0B\u8BFE\u9898\u4E0D\u80FD\u4E3A\u7A7A" : "Course title is required");
|
|
@@ -42159,7 +41891,12 @@ ${timelineText}` : "- \u8BFE\u5802\u65F6\u95F4\u8F74\u73AF\u8282\uFF1A\uFF08\u65
|
|
|
42159
41891
|
- \`## \u{1F4E5} \u8BFE\u540E\u4F5C\u4E1A\`\uFF1A\u4E00\u4EFD\u660E\u786E\u7684\u8BFE\u540E\u4EFB\u52A1\u4E0E\u63D0\u4EA4\u8981\u6C42\u3002
|
|
42160
41892
|
4. \u5185\u5BB9\u5FC5\u987B\u7D27\u6263\u8BFE\u9898\u672C\u8EAB\uFF0C\u7981\u6B62\u4F7F\u7528"\u516C\u5F0F A""\u6A21\u5757 1"\u4E4B\u7C7B\u7684\u5360\u4F4D\u7B26\u6216\u4E0E\u8BFE\u9898\u65E0\u5173\u7684\u901A\u7528\u5957\u8BDD\u3002
|
|
42161
41893
|
5. \u7BC7\u5E45\u63A7\u5236\u5728 600-1000 \u5B57\uFF0C\u8BED\u8A00${lang === "zh" ? "\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587" : "use English"}\u3002`;
|
|
42162
|
-
const raw = await this.aiService.generateText(prompt, {
|
|
41894
|
+
const raw = await this.aiService.generateText(prompt, {
|
|
41895
|
+
temperature: 0.6,
|
|
41896
|
+
maxTokens: 2048,
|
|
41897
|
+
userId,
|
|
41898
|
+
providerId
|
|
41899
|
+
});
|
|
42163
41900
|
const outline = stripReasoningArtifacts(stripOuterCodeFence(String(raw || "").trim()));
|
|
42164
41901
|
if (!outline) {
|
|
42165
41902
|
const err = new Error(
|
|
@@ -42423,13 +42160,17 @@ function registerLessonsRoutes(ctx) {
|
|
|
42423
42160
|
safeAiLimiter,
|
|
42424
42161
|
async (req, res) => {
|
|
42425
42162
|
try {
|
|
42426
|
-
const { title, category, description, timeline, lang } = req.body || {};
|
|
42163
|
+
const { title, category, description, timeline, lang, providerId } = req.body || {};
|
|
42164
|
+
const session = req.session;
|
|
42165
|
+
const userId = session?.userId || session?.username;
|
|
42427
42166
|
const content = await lessonService.generateOutlineWithAi({
|
|
42428
42167
|
title,
|
|
42429
42168
|
category,
|
|
42430
42169
|
description,
|
|
42431
42170
|
timeline,
|
|
42432
|
-
lang
|
|
42171
|
+
lang,
|
|
42172
|
+
providerId,
|
|
42173
|
+
userId
|
|
42433
42174
|
});
|
|
42434
42175
|
res.json({ success: true, content });
|
|
42435
42176
|
} catch (e) {
|
|
@@ -48515,7 +48256,7 @@ var ScheduleService = class {
|
|
|
48515
48256
|
* Timetable OCR 课表图像识别与数据清洗
|
|
48516
48257
|
*/
|
|
48517
48258
|
async processTimetableOcr(imageBase64, options = {}) {
|
|
48518
|
-
const { lang = "zh", providerId } = options;
|
|
48259
|
+
const { lang = "zh", providerId, userId } = options;
|
|
48519
48260
|
if (!imageBase64) {
|
|
48520
48261
|
throw new Error("imageBase64 is required");
|
|
48521
48262
|
}
|
|
@@ -48546,21 +48287,11 @@ var ScheduleService = class {
|
|
|
48546
48287
|
|
|
48547
48288
|
\u8FD4\u56DE\u683C\u5F0F\u793A\u4F8B\uFF1A
|
|
48548
48289
|
[{"dayOfWeek":1,"periodNumber":1,"className":"\u9AD8\u4E00(13)","subject":"\u4FE1\u606F","timeSlot":"08:00-08:40","location":"312","teacherName":""}]`;
|
|
48549
|
-
|
|
48550
|
-
|
|
48551
|
-
|
|
48552
|
-
|
|
48553
|
-
|
|
48554
|
-
provider = this.db.prepare(
|
|
48555
|
-
"SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' ORDER BY created_at DESC LIMIT 1"
|
|
48556
|
-
).get();
|
|
48557
|
-
}
|
|
48558
|
-
if (provider?.api_key) provider.api_key = decryptApiKey(provider.api_key);
|
|
48559
|
-
if (!provider || !provider.api_key || !provider.api_key.trim()) {
|
|
48560
|
-
throw new Error(
|
|
48561
|
-
lang === "zh" ? "\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" : 'No AI provider configured. Please add and configure an AI Provider in "System Management -> AI Provider Management".'
|
|
48562
|
-
);
|
|
48563
|
-
}
|
|
48290
|
+
const provider = resolveAccessibleAiProvider(this.db, {
|
|
48291
|
+
providerId,
|
|
48292
|
+
userId,
|
|
48293
|
+
lang
|
|
48294
|
+
});
|
|
48564
48295
|
let chatUrl = provider.api_url.trim();
|
|
48565
48296
|
if (!chatUrl.endsWith("/chat/completions")) {
|
|
48566
48297
|
chatUrl = chatUrl.endsWith("/") ? chatUrl + "chat/completions" : chatUrl + "/chat/completions";
|
|
@@ -48776,7 +48507,9 @@ function registerSchedulesRoutes(ctx) {
|
|
|
48776
48507
|
const startTime = Date.now();
|
|
48777
48508
|
try {
|
|
48778
48509
|
const { imageBase64, lang = "zh", providerId } = req.body;
|
|
48779
|
-
const
|
|
48510
|
+
const session = req.session;
|
|
48511
|
+
const userId = session?.userId || session?.username;
|
|
48512
|
+
const result = await scheduleService.processTimetableOcr(imageBase64, { lang, providerId, userId });
|
|
48780
48513
|
res.json({
|
|
48781
48514
|
success: true,
|
|
48782
48515
|
entries: result.entries,
|
|
@@ -49162,7 +48895,7 @@ var GradingService = class {
|
|
|
49162
48895
|
}
|
|
49163
48896
|
// ── 6. AI 学期综合总评评语生成 ────────────────────────────────────────────────
|
|
49164
48897
|
async generateSemesterAiEvaluation(params) {
|
|
49165
|
-
const { classId, studentId, semesterName = "2026\u5E74\u6625\u5B63\u5B66\u671F", providerId } = params;
|
|
48898
|
+
const { classId, studentId, semesterName = "2026\u5E74\u6625\u5B63\u5B66\u671F", providerId, userId } = params;
|
|
49166
48899
|
const student = this.db.prepare("SELECT name FROM students WHERE id = ?").get(studentId);
|
|
49167
48900
|
if (!student) {
|
|
49168
48901
|
throw new Error("Student not found");
|
|
@@ -49220,13 +48953,10 @@ ${examsText}
|
|
|
49220
48953
|
- \u6307\u51FA\u5176\u53EF\u4EE5\u6539\u8FDB\u7684\u5730\u65B9\uFF08\u5982\u8FDB\u5EA6\u843D\u540E\u3001\u8003\u8BD5\u53D1\u6325\u4E0D\u4F73\u7B49\uFF09\uFF0C\u8BED\u6C14\u8981\u975E\u5E38\u6E29\u67D4\u3001\u59D4\u5A49\uFF0C\u7ED9\u4E88\u5176\u4FE1\u5FC3\u3002
|
|
49221
48954
|
- \u5BF9\u672A\u6765\u7684\u671F\u8BB8\uFF0C\u6FC0\u52B1\u5B66\u751F\u5728\u4E0B\u5B66\u671F\u7EE7\u7EED\u52AA\u529B\u3002
|
|
49222
48955
|
3. \u957F\u5EA6\u63A7\u5236\u5728 150-250 \u5B57\u4E4B\u95F4\u3002\u4E0D\u8981\u5305\u542B\u4EFB\u4F55 Markdown \u683C\u5F0F\uFF0C\u53EA\u8FD4\u56DE\u7EAF\u6587\u672C\u8BC4\u8BED\u3002`;
|
|
49223
|
-
const provider =
|
|
49224
|
-
|
|
49225
|
-
|
|
49226
|
-
|
|
49227
|
-
if (!provider || !provider.api_key || !provider.api_key.trim()) {
|
|
49228
|
-
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");
|
|
49229
|
-
}
|
|
48956
|
+
const provider = resolveAccessibleAiProvider(this.db, {
|
|
48957
|
+
providerId,
|
|
48958
|
+
userId
|
|
48959
|
+
});
|
|
49230
48960
|
let chatUrl = provider.api_url.trim();
|
|
49231
48961
|
if (!chatUrl.endsWith("/chat/completions")) {
|
|
49232
48962
|
chatUrl = chatUrl.endsWith("/") ? chatUrl + "chat/completions" : chatUrl + "/chat/completions";
|
|
@@ -49499,11 +49229,14 @@ function registerGradingRoutes(ctx) {
|
|
|
49499
49229
|
const { classId, studentId } = req.params;
|
|
49500
49230
|
if (gate(req, res, classId, "staff")) return;
|
|
49501
49231
|
const { semesterName = "2026\u5E74\u6625\u5B63\u5B66\u671F", providerId } = req.body;
|
|
49232
|
+
const session = req.session;
|
|
49233
|
+
const userId = session?.userId || session?.username;
|
|
49502
49234
|
const aiEvaluation = await gradingService.generateSemesterAiEvaluation({
|
|
49503
49235
|
classId,
|
|
49504
49236
|
studentId,
|
|
49505
49237
|
semesterName,
|
|
49506
|
-
providerId
|
|
49238
|
+
providerId,
|
|
49239
|
+
userId
|
|
49507
49240
|
});
|
|
49508
49241
|
res.json({ success: true, aiEvaluation });
|
|
49509
49242
|
} catch (e) {
|
|
@@ -49931,7 +49664,8 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
49931
49664
|
}
|
|
49932
49665
|
}
|
|
49933
49666
|
const clientIp = req.ip || req.socket?.remoteAddress || "unknown-ip";
|
|
49934
|
-
const
|
|
49667
|
+
const identityKey = auth.isAuthenticated ? `u:${String(auth.userId || auth.username || auth.actorId).slice(0, 64)}` : `ip:${clientIp}`;
|
|
49668
|
+
const rateLimitKey = `${identityKey}:${resolvedUuid || pluginId}:${method}:${normalizedSubPath}`;
|
|
49935
49669
|
const windowMs = matchedRule?.rateLimit?.windowMs ?? 60 * 1e3;
|
|
49936
49670
|
const maxRequests = matchedRule?.rateLimit?.max ?? 120;
|
|
49937
49671
|
if (!rateLimiter.consume(rateLimitKey, maxRequests, windowMs)) {
|
|
@@ -50920,16 +50654,9 @@ function listInstalledManifestVersions() {
|
|
|
50920
50654
|
return installed;
|
|
50921
50655
|
}
|
|
50922
50656
|
function registerPluginsRoutes(ctx) {
|
|
50923
|
-
const {
|
|
50924
|
-
|
|
50925
|
-
|
|
50926
|
-
buildOpenAITools: buildOpenAITools2,
|
|
50927
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
50928
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
50929
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
50930
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
50931
|
-
} = ctx;
|
|
50932
|
-
const safeAiLimiter = typeof aiLimiter === "function" ? aiLimiter : (_req, _res, next) => next();
|
|
50657
|
+
const { app, aiLimiter } = ctx;
|
|
50658
|
+
const noopMiddleware = (_req, _res, next) => next();
|
|
50659
|
+
const safeAiLimiter = typeof aiLimiter === "function" ? aiLimiter : noopMiddleware;
|
|
50933
50660
|
app.get("/api/docs/plugin-guide", (req, res) => {
|
|
50934
50661
|
try {
|
|
50935
50662
|
const docPath = import_path22.default.join(process.cwd(), "docs_plugin_guide.md");
|
|
@@ -51965,9 +51692,13 @@ function registerPluginsRoutes(ctx) {
|
|
|
51965
51692
|
}
|
|
51966
51693
|
});
|
|
51967
51694
|
app.all("/api/plugins/:pluginId/*", pluginApiGatewayMiddleware);
|
|
51968
|
-
app.get("/api/ai-providers",
|
|
51695
|
+
app.get("/api/ai-providers", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
|
|
51969
51696
|
try {
|
|
51970
|
-
const
|
|
51697
|
+
const session = req.session;
|
|
51698
|
+
const currentUserId = session?.userId || session?.username;
|
|
51699
|
+
const providers = kernelContainer.db.prepare(
|
|
51700
|
+
"SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at FROM ai_providers WHERE (owner_id = ? OR is_shared = 1) ORDER BY created_at DESC"
|
|
51701
|
+
).all(currentUserId);
|
|
51971
51702
|
const masked = providers.map((p) => {
|
|
51972
51703
|
let apiKey;
|
|
51973
51704
|
try {
|
|
@@ -51975,54 +51706,122 @@ function registerPluginsRoutes(ctx) {
|
|
|
51975
51706
|
} catch {
|
|
51976
51707
|
apiKey = "[UNDECRYPTABLE]";
|
|
51977
51708
|
}
|
|
51978
|
-
return {
|
|
51709
|
+
return {
|
|
51710
|
+
...p,
|
|
51711
|
+
api_key: apiKey,
|
|
51712
|
+
is_shared: Number(p.is_shared) === 1
|
|
51713
|
+
};
|
|
51979
51714
|
});
|
|
51980
51715
|
res.json(masked);
|
|
51981
51716
|
} catch (e) {
|
|
51982
51717
|
sendSafeError(res, e);
|
|
51983
51718
|
}
|
|
51984
51719
|
});
|
|
51985
|
-
app.post("/api/ai-providers",
|
|
51720
|
+
app.post("/api/ai-providers", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
|
|
51986
51721
|
try {
|
|
51987
|
-
const
|
|
51722
|
+
const session = req.session;
|
|
51723
|
+
const currentUserId = session?.userId || session?.username;
|
|
51724
|
+
const isAdmin = isAdministrator2(session);
|
|
51725
|
+
const { name, api_url, api_key, model_name, is_shared } = req.body;
|
|
51988
51726
|
if (!name || !api_url || !model_name) {
|
|
51989
51727
|
return res.status(400).json({ error: "Missing name, api_url or model_name" });
|
|
51990
51728
|
}
|
|
51729
|
+
const wantsShared = is_shared === 1 || is_shared === true || is_shared === "1";
|
|
51730
|
+
if (wantsShared && !isAdmin) {
|
|
51731
|
+
return res.status(403).json({ error: "Only administrators can create platform shared providers" });
|
|
51732
|
+
}
|
|
51991
51733
|
const id = "prov_" + Date.now();
|
|
51992
51734
|
const now = Date.now();
|
|
51993
51735
|
const encryptedKey = api_key ? encryptApiKey(api_key) : "";
|
|
51994
51736
|
kernelContainer.db.prepare(
|
|
51995
|
-
"INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
51996
|
-
).run(
|
|
51737
|
+
"INSERT INTO ai_providers (id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
51738
|
+
).run(
|
|
51739
|
+
id,
|
|
51740
|
+
name.trim(),
|
|
51741
|
+
api_url.trim(),
|
|
51742
|
+
encryptedKey,
|
|
51743
|
+
model_name.trim(),
|
|
51744
|
+
currentUserId,
|
|
51745
|
+
wantsShared && isAdmin ? 1 : 0,
|
|
51746
|
+
now,
|
|
51747
|
+
now
|
|
51748
|
+
);
|
|
51997
51749
|
res.json({ success: true, id });
|
|
51998
51750
|
} catch (e) {
|
|
51999
51751
|
sendSafeError(res, e);
|
|
52000
51752
|
}
|
|
52001
51753
|
});
|
|
52002
|
-
app.put("/api/ai-providers/:id",
|
|
51754
|
+
app.put("/api/ai-providers/:id", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
|
|
52003
51755
|
try {
|
|
52004
|
-
const
|
|
51756
|
+
const session = req.session;
|
|
51757
|
+
const currentUserId = session?.userId || session?.username;
|
|
51758
|
+
const isAdmin = isAdministrator2(session);
|
|
51759
|
+
const { name, api_url, api_key, model_name, is_shared } = req.body;
|
|
52005
51760
|
if (!name || !api_url || !model_name) {
|
|
52006
51761
|
return res.status(400).json({ error: "Missing name, api_url or model_name" });
|
|
52007
51762
|
}
|
|
51763
|
+
const existing = kernelContainer.db.prepare("SELECT id, api_key, owner_id, is_shared FROM ai_providers WHERE id = ?").get(req.params.id);
|
|
51764
|
+
if (!existing) {
|
|
51765
|
+
return res.status(404).json({ error: "AI provider not found" });
|
|
51766
|
+
}
|
|
51767
|
+
const isExistingShared = Number(existing.is_shared) === 1;
|
|
51768
|
+
if (isExistingShared) {
|
|
51769
|
+
if (!isAdmin) {
|
|
51770
|
+
return res.status(403).json({ error: "Forbidden: Only administrators can modify platform shared providers" });
|
|
51771
|
+
}
|
|
51772
|
+
} else {
|
|
51773
|
+
if (existing.owner_id !== currentUserId) {
|
|
51774
|
+
return res.status(403).json({
|
|
51775
|
+
error: "Forbidden: You do not have permission to modify another user's private AI provider"
|
|
51776
|
+
});
|
|
51777
|
+
}
|
|
51778
|
+
}
|
|
51779
|
+
let nextShared = existing.is_shared ?? 0;
|
|
51780
|
+
if (is_shared !== void 0) {
|
|
51781
|
+
const wantsShared = is_shared === 1 || is_shared === true || is_shared === "1";
|
|
51782
|
+
if (wantsShared !== (Number(existing.is_shared) === 1)) {
|
|
51783
|
+
if (!isAdmin) {
|
|
51784
|
+
return res.status(403).json({ error: "Forbidden: Only administrators can change platform shared status" });
|
|
51785
|
+
}
|
|
51786
|
+
nextShared = wantsShared ? 1 : 0;
|
|
51787
|
+
}
|
|
51788
|
+
}
|
|
52008
51789
|
const now = Date.now();
|
|
52009
51790
|
let finalKey;
|
|
52010
51791
|
if (api_key && api_key.trim() !== "" && !api_key.includes("****")) {
|
|
52011
51792
|
finalKey = encryptApiKey(api_key);
|
|
52012
51793
|
} else {
|
|
52013
|
-
const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ?").get(req.params.id);
|
|
52014
51794
|
finalKey = existing?.api_key || "";
|
|
52015
51795
|
}
|
|
52016
51796
|
kernelContainer.db.prepare(
|
|
52017
|
-
"UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?"
|
|
52018
|
-
).run(name, api_url, finalKey, model_name, now, req.params.id);
|
|
51797
|
+
"UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, is_shared = ?, updated_at = ? WHERE id = ?"
|
|
51798
|
+
).run(name.trim(), api_url.trim(), finalKey, model_name.trim(), nextShared, now, req.params.id);
|
|
52019
51799
|
res.json({ success: true });
|
|
52020
51800
|
} catch (e) {
|
|
52021
51801
|
sendSafeError(res, e);
|
|
52022
51802
|
}
|
|
52023
51803
|
});
|
|
52024
|
-
app.delete("/api/ai-providers/:id",
|
|
51804
|
+
app.delete("/api/ai-providers/:id", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
|
|
52025
51805
|
try {
|
|
51806
|
+
const session = req.session;
|
|
51807
|
+
const currentUserId = session?.userId || session?.username;
|
|
51808
|
+
const isAdmin = isAdministrator2(session);
|
|
51809
|
+
const existing = kernelContainer.db.prepare("SELECT id, owner_id, is_shared FROM ai_providers WHERE id = ?").get(req.params.id);
|
|
51810
|
+
if (!existing) {
|
|
51811
|
+
return res.status(404).json({ error: "AI provider not found" });
|
|
51812
|
+
}
|
|
51813
|
+
const isExistingShared = Number(existing.is_shared) === 1;
|
|
51814
|
+
if (isExistingShared) {
|
|
51815
|
+
if (!isAdmin) {
|
|
51816
|
+
return res.status(403).json({ error: "Forbidden: Only administrators can delete platform shared providers" });
|
|
51817
|
+
}
|
|
51818
|
+
} else {
|
|
51819
|
+
if (existing.owner_id !== currentUserId) {
|
|
51820
|
+
return res.status(403).json({
|
|
51821
|
+
error: "Forbidden: You do not have permission to delete another user's private AI provider"
|
|
51822
|
+
});
|
|
51823
|
+
}
|
|
51824
|
+
}
|
|
52026
51825
|
kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
|
|
52027
51826
|
res.json({ success: true });
|
|
52028
51827
|
} catch (e) {
|
|
@@ -52105,65 +51904,82 @@ function registerPluginsRoutes(ctx) {
|
|
|
52105
51904
|
sendSafeError(res, e);
|
|
52106
51905
|
}
|
|
52107
51906
|
});
|
|
52108
|
-
app.post(
|
|
52109
|
-
|
|
52110
|
-
|
|
52111
|
-
|
|
52112
|
-
|
|
52113
|
-
}
|
|
52114
|
-
let api_key = "";
|
|
52115
|
-
if (providedKey && providedKey.includes("****")) {
|
|
52116
|
-
const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? LIMIT 1").get(api_url, model_name);
|
|
52117
|
-
api_key = existing ? decryptApiKey(existing.api_key) : "";
|
|
52118
|
-
} else if (providedKey) {
|
|
52119
|
-
api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
|
|
52120
|
-
}
|
|
52121
|
-
let cleanUrl = api_url.trim();
|
|
52122
|
-
if (!cleanUrl.endsWith("/chat/completions")) {
|
|
52123
|
-
cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
|
|
52124
|
-
}
|
|
52125
|
-
const urlSafety = isSafeExternalUrl(cleanUrl);
|
|
52126
|
-
if (!urlSafety.safe) {
|
|
52127
|
-
return res.status(400).json({ error: `Blocked unsafe AI provider URL: ${urlSafety.reason}` });
|
|
52128
|
-
}
|
|
52129
|
-
let response;
|
|
51907
|
+
app.post(
|
|
51908
|
+
"/api/ai-providers/test",
|
|
51909
|
+
requireScopedAuth("self", "teacher", "administrator"),
|
|
51910
|
+
safeAiLimiter,
|
|
51911
|
+
async (req, res) => {
|
|
52130
51912
|
try {
|
|
52131
|
-
|
|
52132
|
-
|
|
52133
|
-
|
|
52134
|
-
|
|
52135
|
-
|
|
52136
|
-
|
|
52137
|
-
|
|
52138
|
-
|
|
52139
|
-
|
|
52140
|
-
|
|
52141
|
-
|
|
52142
|
-
|
|
52143
|
-
|
|
52144
|
-
|
|
52145
|
-
|
|
52146
|
-
|
|
52147
|
-
|
|
51913
|
+
const session = req.session;
|
|
51914
|
+
const currentUserId = session?.userId || session?.username;
|
|
51915
|
+
const { api_url, api_key: providedKey, model_name, id } = req.body;
|
|
51916
|
+
if (!api_url || !model_name) {
|
|
51917
|
+
return res.status(400).json({ error: "api_url and model_name are required" });
|
|
51918
|
+
}
|
|
51919
|
+
let api_key = "";
|
|
51920
|
+
if (providedKey && providedKey.includes("****")) {
|
|
51921
|
+
let existing;
|
|
51922
|
+
if (id) {
|
|
51923
|
+
existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ? AND (owner_id = ? OR is_shared = 1)").get(id, currentUserId);
|
|
51924
|
+
} else {
|
|
51925
|
+
existing = kernelContainer.db.prepare(
|
|
51926
|
+
"SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? AND (owner_id = ? OR is_shared = 1) LIMIT 1"
|
|
51927
|
+
).get(api_url, model_name, currentUserId);
|
|
51928
|
+
}
|
|
51929
|
+
if (!existing) {
|
|
51930
|
+
return res.status(403).json({ error: "Forbidden: Cannot access key for this provider" });
|
|
51931
|
+
}
|
|
51932
|
+
api_key = decryptApiKey(existing.api_key);
|
|
51933
|
+
} else if (providedKey) {
|
|
51934
|
+
api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
|
|
51935
|
+
}
|
|
51936
|
+
let cleanUrl = api_url.trim();
|
|
51937
|
+
if (!cleanUrl.endsWith("/chat/completions")) {
|
|
51938
|
+
cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
|
|
51939
|
+
}
|
|
51940
|
+
const urlSafety = isSafeExternalUrl(cleanUrl);
|
|
51941
|
+
if (!urlSafety.safe) {
|
|
51942
|
+
return res.status(400).json({ error: `Blocked unsafe AI provider URL: ${urlSafety.reason}` });
|
|
51943
|
+
}
|
|
51944
|
+
let response;
|
|
51945
|
+
try {
|
|
51946
|
+
response = await fetchWithSafeRedirects(cleanUrl, {
|
|
51947
|
+
method: "POST",
|
|
51948
|
+
headers: {
|
|
51949
|
+
"Content-Type": "application/json",
|
|
51950
|
+
Authorization: `Bearer ${api_key || ""}`
|
|
51951
|
+
},
|
|
51952
|
+
body: JSON.stringify({
|
|
51953
|
+
model: model_name,
|
|
51954
|
+
messages: [{ role: "user", content: "Say connected" }],
|
|
51955
|
+
max_tokens: 5
|
|
51956
|
+
}),
|
|
51957
|
+
timeoutMs: 1e4
|
|
51958
|
+
});
|
|
51959
|
+
} catch (err) {
|
|
51960
|
+
const isTimeout = err instanceof AIFetchTimeoutError || err?.name === "TimeoutError" || err?.name === "AbortError" || /abort|timeout/i.test(err?.message ?? "");
|
|
51961
|
+
if (isTimeout) {
|
|
51962
|
+
return res.status(504).json({
|
|
51963
|
+
success: false,
|
|
51964
|
+
error: "AI Provider request timed out after 10000ms. Please verify the URL or provider availability."
|
|
51965
|
+
});
|
|
51966
|
+
}
|
|
51967
|
+
throw err;
|
|
51968
|
+
}
|
|
51969
|
+
const responseText = await response.text();
|
|
51970
|
+
if (response.ok) {
|
|
51971
|
+
res.json({ success: true, message: "Successfully connected and received response." });
|
|
51972
|
+
} else {
|
|
51973
|
+
res.status(response.status).json({
|
|
52148
51974
|
success: false,
|
|
52149
|
-
error:
|
|
51975
|
+
error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}`
|
|
52150
51976
|
});
|
|
52151
51977
|
}
|
|
52152
|
-
|
|
52153
|
-
|
|
52154
|
-
const responseText = await response.text();
|
|
52155
|
-
if (response.ok) {
|
|
52156
|
-
res.json({ success: true, message: "Successfully connected and received response." });
|
|
52157
|
-
} else {
|
|
52158
|
-
res.status(response.status).json({
|
|
52159
|
-
success: false,
|
|
52160
|
-
error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}`
|
|
52161
|
-
});
|
|
51978
|
+
} catch (e) {
|
|
51979
|
+
sendSafeError(res, e, 500, "Connection failed");
|
|
52162
51980
|
}
|
|
52163
|
-
} catch (e) {
|
|
52164
|
-
sendSafeError(res, e, 500, "Connection failed");
|
|
52165
51981
|
}
|
|
52166
|
-
|
|
51982
|
+
);
|
|
52167
51983
|
}
|
|
52168
51984
|
|
|
52169
51985
|
// server/services/assessment-service.ts
|
|
@@ -58608,12 +58424,16 @@ async function startServer() {
|
|
|
58608
58424
|
const defaultWriteLimit = process.env.PLAYWRIGHT_TEST ? 1e4 : isDev ? 600 : 180;
|
|
58609
58425
|
const writeLimiter = createWriteLimiter(defaultWriteLimit);
|
|
58610
58426
|
const viewStateLimiter = createWriteLimiter(process.env.PLAYWRIGHT_TEST ? 1e4 : isDev ? 600 : 120);
|
|
58427
|
+
const pluginApiLimiter = createSessionWriteLimiter(process.env.PLAYWRIGHT_TEST ? 1e4 : 600);
|
|
58611
58428
|
const aiLimiter = createAiLimiter(process.env.PLAYWRIGHT_TEST ? 1e3 : 10);
|
|
58612
58429
|
app.use((req, res, next) => {
|
|
58613
58430
|
if (req.method === "POST" || req.method === "PUT" || req.method === "DELETE") {
|
|
58614
58431
|
if (/^\/api\/classroom\/sessions\/[^/]+\/view-state\/?$/.test(req.path)) {
|
|
58615
58432
|
return viewStateLimiter(req, res, next);
|
|
58616
58433
|
}
|
|
58434
|
+
if (req.path.startsWith("/api/plugins/")) {
|
|
58435
|
+
return pluginApiLimiter(req, res, next);
|
|
58436
|
+
}
|
|
58617
58437
|
return writeLimiter(req, res, next);
|
|
58618
58438
|
}
|
|
58619
58439
|
next();
|