openlearn-next 0.1.16 → 0.2.2
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 +2 -0
- package/dist/assets/{courseware-fTX0oFYe.js → courseware-CL7gjXZj.js} +1 -1
- package/dist/assets/index-4xDvXGZ_.js +682 -0
- package/dist/assets/index-CaJDJGky.css +1 -0
- package/dist/assets/{index.es-BLnapfB6.js → index.es-D071ILbD.js} +1 -1
- package/dist/assets/{vendor-konva-BRP-lFzZ.js → vendor-konva-GivUq6dt.js} +1 -1
- package/dist/assets/{vendor-pptx-w8e1eCEK.js → vendor-pptx-CJ3I-KT0.js} +5 -5
- package/dist/assets/{whiteboard-DzrvcsKx.js → whiteboard-Dp8tFXTO.js} +58 -23
- package/dist/index.html +2 -2
- package/dist/server.cjs +2049 -1935
- package/package.json +6 -2
- package/dist/assets/index-BWnZ8DnC.js +0 -799
- package/dist/assets/index-cs3Y-roi.css +0 -1
package/dist/server.cjs
CHANGED
|
@@ -1036,16 +1036,18 @@ function wrapProcessManager(processService, tracker, pluginId) {
|
|
|
1036
1036
|
}),
|
|
1037
1037
|
registerInterval: createSafeFunction(
|
|
1038
1038
|
(name, intervalMs, tickFn) => {
|
|
1039
|
-
return
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1039
|
+
return Promise.resolve(
|
|
1040
|
+
processService.registerInterval(name, intervalMs, (log) => {
|
|
1041
|
+
try {
|
|
1042
|
+
tickFn(log);
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
console.error(`[Plugin:${pluginId}] Error in interval task ${name}:`, e);
|
|
1045
|
+
}
|
|
1046
|
+
})
|
|
1047
|
+
).then((processId) => {
|
|
1046
1048
|
tracker.track(pluginId, {
|
|
1047
1049
|
dispose: () => {
|
|
1048
|
-
processService.kill(processId).catch(() => {
|
|
1050
|
+
Promise.resolve(processService.kill(processId)).catch(() => {
|
|
1049
1051
|
});
|
|
1050
1052
|
}
|
|
1051
1053
|
});
|
|
@@ -3169,6 +3171,16 @@ var init_worker_manager = __esm({
|
|
|
3169
3171
|
}
|
|
3170
3172
|
return await this.registry.terminate(pluginId);
|
|
3171
3173
|
}
|
|
3174
|
+
/**
|
|
3175
|
+
* 中止所有活跃 Worker 线程(有序关闭)。
|
|
3176
|
+
*
|
|
3177
|
+
* 遍历 WorkerRegistry 中当前活跃的 Worker,逐个调用 terminate() 释放线程资源。
|
|
3178
|
+
* 供 PluginRuntimeComposition 在平台停机阶段调用。
|
|
3179
|
+
*/
|
|
3180
|
+
async shutdownAll() {
|
|
3181
|
+
const ids = this.registry.list();
|
|
3182
|
+
await Promise.all(ids.map((id) => this.terminateWorker(id)));
|
|
3183
|
+
}
|
|
3172
3184
|
/**
|
|
3173
3185
|
* 从数据库恢复所有 worker-mode 的活跃插件。
|
|
3174
3186
|
*
|
|
@@ -3687,6 +3699,7 @@ var manifestSchema = import_zod.z.object({
|
|
|
3687
3699
|
})).optional()
|
|
3688
3700
|
}).optional(),
|
|
3689
3701
|
contributes: contributesSchema,
|
|
3702
|
+
classroomTools: import_zod.z.array(import_zod.z.unknown()).optional(),
|
|
3690
3703
|
deploy: deploySchema
|
|
3691
3704
|
}).passthrough();
|
|
3692
3705
|
var requiresItemV3Schema = import_zod.z.string().regex(
|
|
@@ -6918,7 +6931,8 @@ var AiPlannerPlugin = {
|
|
|
6918
6931
|
const eventBus = ctx.services.eventBus;
|
|
6919
6932
|
const db2 = await ctx.resolve(import_plugin_sdk5.IDatabaseToken);
|
|
6920
6933
|
const processManager = ctx.services.processManager;
|
|
6921
|
-
await processManager.registerHandler("ai_planner_task", async (processId,
|
|
6934
|
+
await processManager.registerHandler("ai_planner_task", async (processId, payloadArg, state, log, updateState) => {
|
|
6935
|
+
const payload = payloadArg;
|
|
6922
6936
|
log(`[AI Planner] Started generation task: ${payload.taskType}`);
|
|
6923
6937
|
const duration = payload.duration || 5;
|
|
6924
6938
|
for (let i = 0; i < duration; i++) {
|
|
@@ -6929,7 +6943,7 @@ var AiPlannerPlugin = {
|
|
|
6929
6943
|
}
|
|
6930
6944
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
6931
6945
|
updateState({ step: i + 1 });
|
|
6932
|
-
log(`[AI Planner] Analyzing
|
|
6946
|
+
log(`[AI Planner] Analyzing payload... step ${i + 1}/${duration}`);
|
|
6933
6947
|
}
|
|
6934
6948
|
log(`[AI Planner] Analysis complete. Generating proposal...`);
|
|
6935
6949
|
const proposalTitle = `AI Suggested ${payload.taskType === "quiz" ? "Quiz" : "Plan"}: ${payload.topic}`;
|
|
@@ -9862,7 +9876,7 @@ var PluginHost = class {
|
|
|
9862
9876
|
}
|
|
9863
9877
|
const previousStatus = existingRow.status;
|
|
9864
9878
|
const currentState = this.pluginStates.get(pluginId) ?? "installed" /* INSTALLED */;
|
|
9865
|
-
const wasActive = currentState === "active" /* ACTIVE */
|
|
9879
|
+
const wasActive = currentState === "active" /* ACTIVE */ ? true : previousStatus === "active";
|
|
9866
9880
|
const oldMode = this.getExecutionMode(pluginId) || "inline";
|
|
9867
9881
|
const executionMode = options.executionMode ?? (manifest.executionMode === "worker" ? "worker" : oldMode || "inline");
|
|
9868
9882
|
const pluginDir = this.getPluginDir(pluginId);
|
|
@@ -16694,11 +16708,12 @@ var PluginCompositionModule = class {
|
|
|
16694
16708
|
}
|
|
16695
16709
|
compose(options) {
|
|
16696
16710
|
const serviceRegistry = getPlatformServiceRegistry();
|
|
16697
|
-
const registerOrReplace = (
|
|
16698
|
-
|
|
16699
|
-
|
|
16711
|
+
const registerOrReplace = (config) => {
|
|
16712
|
+
const { id, instance } = config;
|
|
16713
|
+
if (serviceRegistry.has(id)) {
|
|
16714
|
+
serviceRegistry.replace(id, instance);
|
|
16700
16715
|
} else {
|
|
16701
|
-
serviceRegistry.register(
|
|
16716
|
+
serviceRegistry.register({ id }, instance);
|
|
16702
16717
|
}
|
|
16703
16718
|
};
|
|
16704
16719
|
const capabilityRegistry = new CapabilityRegistry();
|
|
@@ -16753,7 +16768,6 @@ var PluginCompositionModule = class {
|
|
|
16753
16768
|
description: "OpenLearn Plugin Distribution Manager Foundation",
|
|
16754
16769
|
instance: realDistributionManager
|
|
16755
16770
|
});
|
|
16756
|
-
capabilityRegistry.registerCapability(new PluginCapability());
|
|
16757
16771
|
permissionManager.register({
|
|
16758
16772
|
id: "perm_plugin_execute",
|
|
16759
16773
|
name: "Plugin Execution Permission",
|
|
@@ -17096,996 +17110,1705 @@ function validateJsonSchema(data, schema) {
|
|
|
17096
17110
|
}
|
|
17097
17111
|
|
|
17098
17112
|
// server.ts
|
|
17099
|
-
var import_genai6 = require("@google/genai");
|
|
17100
|
-
var import_crypto13 = __toESM(require("crypto"), 1);
|
|
17101
17113
|
var import_helmet = __toESM(require("helmet"), 1);
|
|
17102
17114
|
var import_express_rate_limit = __toESM(require("express-rate-limit"), 1);
|
|
17103
17115
|
|
|
17104
|
-
//
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17116
|
+
// server/realtime-bridge.ts
|
|
17117
|
+
function setupRealtimeBridge({ eventBus, io, db: db2 }) {
|
|
17118
|
+
eventBus.subscribe("assignment.graded", (event) => {
|
|
17119
|
+
try {
|
|
17120
|
+
const payload = event.payload;
|
|
17121
|
+
const assignment = db2.prepare("SELECT title FROM assignments WHERE id = ?").get(payload.assignmentId);
|
|
17122
|
+
const assignmentTitle = assignment ? assignment.title : "Assignment";
|
|
17123
|
+
console.log(`[EventBus -> Socket.IO] Broadcasting assignment-graded-toast to student ${payload.studentId}`);
|
|
17124
|
+
io.emit("assignment-graded-toast", {
|
|
17125
|
+
assignmentId: payload.assignmentId,
|
|
17126
|
+
assignmentTitle,
|
|
17127
|
+
studentId: payload.studentId,
|
|
17128
|
+
score: payload.score,
|
|
17129
|
+
feedback: payload.feedback || ""
|
|
17130
|
+
});
|
|
17131
|
+
} catch (e) {
|
|
17132
|
+
console.error("[EventBus -> Socket.IO] Error dispatching assignment graded notification:", e);
|
|
17117
17133
|
}
|
|
17118
|
-
}
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17134
|
+
});
|
|
17135
|
+
const handleRollcallElement = (elementId) => {
|
|
17136
|
+
try {
|
|
17137
|
+
const el = db2.prepare("SELECT * FROM whiteboard_elements WHERE id = ?").get(elementId);
|
|
17138
|
+
if (el && el.type === "rollcall") {
|
|
17139
|
+
const elData = JSON.parse(el.data);
|
|
17140
|
+
if (elData && elData.selectedStudent && elData.status === "picked") {
|
|
17141
|
+
const studentId = elData.selectedStudent.id;
|
|
17142
|
+
const studentName = elData.selectedStudent.name;
|
|
17143
|
+
let classId = elData.classId || "";
|
|
17144
|
+
const lessonId = el.lesson_id;
|
|
17145
|
+
if (!classId && lessonId) {
|
|
17146
|
+
const sched = db2.prepare("SELECT class_id FROM schedules WHERE lesson_id = ? LIMIT 1").get(lessonId);
|
|
17147
|
+
if (sched) {
|
|
17148
|
+
classId = sched.class_id;
|
|
17149
|
+
}
|
|
17150
|
+
}
|
|
17151
|
+
const pickedTimeStr = elData.pickedTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
17152
|
+
const pickedTime = new Date(pickedTimeStr).getTime();
|
|
17153
|
+
const rollcallId = `rollcall-${elementId}-${pickedTime}`;
|
|
17154
|
+
const exists = db2.prepare("SELECT id FROM student_rollcalls WHERE id = ?").get(rollcallId);
|
|
17155
|
+
if (!exists) {
|
|
17156
|
+
db2.prepare(
|
|
17157
|
+
"INSERT INTO student_rollcalls (id, student_id, class_id, lesson_id, picked_time) VALUES (?, ?, ?, ?, ?)"
|
|
17158
|
+
).run(rollcallId, studentId, classId, lessonId, pickedTime);
|
|
17159
|
+
console.log(`[Rollcall] Saved rollcall for student ${studentId} (${studentName})`);
|
|
17160
|
+
io.emit("student-picked", {
|
|
17161
|
+
rollcallId,
|
|
17162
|
+
studentId,
|
|
17163
|
+
studentName,
|
|
17164
|
+
classId,
|
|
17165
|
+
lessonId,
|
|
17166
|
+
pickedTime
|
|
17167
|
+
});
|
|
17168
|
+
}
|
|
17169
|
+
}
|
|
17123
17170
|
}
|
|
17171
|
+
} catch (e) {
|
|
17172
|
+
console.error("Error handling rollcall element:", e);
|
|
17124
17173
|
}
|
|
17125
|
-
|
|
17126
|
-
|
|
17174
|
+
};
|
|
17175
|
+
eventBus.subscribe("whiteboard.element_drawn", (event) => {
|
|
17176
|
+
try {
|
|
17177
|
+
const payload = event.payload;
|
|
17178
|
+
if (payload.type === "rollcall") {
|
|
17179
|
+
handleRollcallElement(payload.elementId);
|
|
17180
|
+
}
|
|
17181
|
+
if (payload.lessonId) {
|
|
17182
|
+
const syncMsg = { roomId: payload.lessonId, type: "refresh" };
|
|
17183
|
+
io.to(payload.lessonId).emit("whiteboard-sync", syncMsg);
|
|
17184
|
+
io.to("whiteboard-broadcast").emit("whiteboard-sync", syncMsg);
|
|
17185
|
+
console.log(`[EventBus -> Socket.IO] Broadcast whiteboard refresh for lesson "${payload.lessonId}" (element: "${payload.elementId}", type: "${payload.type}")`);
|
|
17186
|
+
}
|
|
17187
|
+
} catch (e) {
|
|
17188
|
+
console.error("[EventBus -> Socket.IO] Error processing whiteboard.element_drawn:", e);
|
|
17127
17189
|
}
|
|
17128
|
-
|
|
17129
|
-
|
|
17190
|
+
});
|
|
17191
|
+
eventBus.subscribe("whiteboard.element_updated", (event) => {
|
|
17192
|
+
try {
|
|
17193
|
+
const payload = event.payload;
|
|
17194
|
+
handleRollcallElement(payload.elementId);
|
|
17195
|
+
} catch (e) {
|
|
17196
|
+
console.error("[EventBus -> Socket.IO] Error processing whiteboard.element_updated for rollcall:", e);
|
|
17130
17197
|
}
|
|
17131
|
-
|
|
17132
|
-
|
|
17198
|
+
});
|
|
17199
|
+
eventBus.subscribe("whiteboard.batch_drawn", (event) => {
|
|
17200
|
+
try {
|
|
17201
|
+
const payload = event.payload;
|
|
17202
|
+
if (payload.lessonId) {
|
|
17203
|
+
io.to(payload.lessonId).emit("whiteboard-sync", {
|
|
17204
|
+
roomId: payload.lessonId,
|
|
17205
|
+
type: "refresh"
|
|
17206
|
+
});
|
|
17207
|
+
console.log(`[EventBus -> Socket.IO] Broadcast refresh after batch_draw (${payload.count} elements) for lesson "${payload.lessonId}"`);
|
|
17208
|
+
}
|
|
17209
|
+
} catch (e) {
|
|
17210
|
+
console.error("[EventBus -> Socket.IO] Error processing whiteboard.batch_drawn:", e);
|
|
17133
17211
|
}
|
|
17134
|
-
}
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
this.configure(options);
|
|
17212
|
+
});
|
|
17213
|
+
eventBus.subscribe("whiteboard.element_deleted", (event) => {
|
|
17214
|
+
try {
|
|
17215
|
+
const payload = event.payload;
|
|
17216
|
+
if (payload.lessonId) {
|
|
17217
|
+
io.to(payload.lessonId).emit("whiteboard-sync", {
|
|
17218
|
+
roomId: payload.lessonId,
|
|
17219
|
+
type: "refresh"
|
|
17220
|
+
});
|
|
17221
|
+
}
|
|
17222
|
+
} catch (e) {
|
|
17223
|
+
console.error("[EventBus -> Socket.IO] Error processing whiteboard.element_deleted:", e);
|
|
17147
17224
|
}
|
|
17148
|
-
}
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
17157
|
-
|
|
17158
|
-
|
|
17159
|
-
|
|
17160
|
-
return this;
|
|
17161
|
-
}
|
|
17162
|
-
registerStages() {
|
|
17163
|
-
BootstrapRegistration.registerExistingBootstrapStages(this._builder);
|
|
17164
|
-
this._state = "PipelineRegistered";
|
|
17165
|
-
return this;
|
|
17166
|
-
}
|
|
17167
|
-
async runBootstrap(context) {
|
|
17168
|
-
if (this._state === "Created") {
|
|
17169
|
-
this.configure(context);
|
|
17225
|
+
});
|
|
17226
|
+
eventBus.subscribe("whiteboard.cleared", (event) => {
|
|
17227
|
+
try {
|
|
17228
|
+
const payload = event.payload;
|
|
17229
|
+
if (payload.lessonId) {
|
|
17230
|
+
io.to(payload.lessonId).emit("whiteboard-sync", {
|
|
17231
|
+
roomId: payload.lessonId,
|
|
17232
|
+
type: "refresh"
|
|
17233
|
+
});
|
|
17234
|
+
}
|
|
17235
|
+
} catch (e) {
|
|
17236
|
+
console.error("[EventBus -> Socket.IO] Error processing whiteboard.cleared:", e);
|
|
17170
17237
|
}
|
|
17171
|
-
|
|
17172
|
-
|
|
17238
|
+
});
|
|
17239
|
+
eventBus.subscribe("spotlight:state_updated", (event) => {
|
|
17240
|
+
try {
|
|
17241
|
+
io.emit("spotlight:state_updated", event.payload);
|
|
17242
|
+
} catch (e) {
|
|
17243
|
+
console.error("[EventBus -> Socket.IO] Error processing spotlight:state_updated:", e);
|
|
17173
17244
|
}
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
} },
|
|
17181
|
-
isCancelled: false,
|
|
17182
|
-
platformContext: builderResult.platformContext,
|
|
17183
|
-
config: builderResult.platformContext.config,
|
|
17184
|
-
state: "Active",
|
|
17185
|
-
currentStage: builderResult.platformContext.currentStage,
|
|
17186
|
-
startTime: Date.now(),
|
|
17187
|
-
getMetadata: () => void 0,
|
|
17188
|
-
setStage: () => {
|
|
17189
|
-
}
|
|
17190
|
-
});
|
|
17191
|
-
if (pipelineResult.status === "Failed") {
|
|
17192
|
-
throw pipelineResult.error || new Error(`ServerBootstrapAdapter pipeline failed at stage: ${pipelineResult.failedStage}`);
|
|
17245
|
+
});
|
|
17246
|
+
eventBus.subscribe("spotlight.state_updated", (event) => {
|
|
17247
|
+
try {
|
|
17248
|
+
io.emit("spotlight:state_updated", event.payload);
|
|
17249
|
+
} catch (e) {
|
|
17250
|
+
console.error("[EventBus -> Socket.IO] Error processing spotlight.state_updated:", e);
|
|
17193
17251
|
}
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
}
|
|
17197
|
-
static async bootstrap(context) {
|
|
17198
|
-
const adapter = _ServerBootstrapAdapter.create(context);
|
|
17199
|
-
return adapter.runBootstrap(context);
|
|
17200
|
-
}
|
|
17201
|
-
};
|
|
17252
|
+
});
|
|
17253
|
+
}
|
|
17202
17254
|
|
|
17203
|
-
//
|
|
17204
|
-
|
|
17255
|
+
// server/shared-state.ts
|
|
17256
|
+
var MF_REMOTE_CACHE = /* @__PURE__ */ new Map();
|
|
17257
|
+
var lessonActiveSegments = /* @__PURE__ */ new Map();
|
|
17205
17258
|
|
|
17206
|
-
//
|
|
17207
|
-
function
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17259
|
+
// server/presence.ts
|
|
17260
|
+
function setupPresence({ io, eventBus }) {
|
|
17261
|
+
const onlineStudents = /* @__PURE__ */ new Map();
|
|
17262
|
+
const activeStudentLessons = /* @__PURE__ */ new Map();
|
|
17263
|
+
const broadcastPresence = () => {
|
|
17264
|
+
io.emit("presence-update", {
|
|
17265
|
+
onlineStudentIds: Array.from(onlineStudents.keys()),
|
|
17266
|
+
activeStudentLessons: Object.fromEntries(activeStudentLessons.entries())
|
|
17267
|
+
});
|
|
17215
17268
|
};
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17222
|
-
|
|
17223
|
-
STARTED: "activity.started",
|
|
17224
|
-
PAUSED: "activity.paused",
|
|
17225
|
-
RESUMED: "activity.resumed",
|
|
17226
|
-
FINISHED: "activity.finished",
|
|
17227
|
-
DISPOSED: "activity.disposed"
|
|
17228
|
-
};
|
|
17229
|
-
function publishEvent(context, type, descriptor, payload) {
|
|
17230
|
-
context.eventBus.publish({
|
|
17231
|
-
id: (0, import_uuid15.v7)(),
|
|
17232
|
-
type,
|
|
17233
|
-
source: `activity:${descriptor.id}`,
|
|
17234
|
-
payload,
|
|
17235
|
-
timestamp: Date.now()
|
|
17236
|
-
});
|
|
17237
|
-
}
|
|
17238
|
-
var BaseActivityProvider = class {
|
|
17239
|
-
constructor(options) {
|
|
17240
|
-
this._state = "registered";
|
|
17241
|
-
if (!options?.descriptor?.id) {
|
|
17242
|
-
throw new Error("BaseActivityProvider requires a descriptor with a valid id.");
|
|
17243
|
-
}
|
|
17244
|
-
this.descriptor = options.descriptor;
|
|
17245
|
-
this.hooks = options;
|
|
17246
|
-
}
|
|
17247
|
-
get state() {
|
|
17248
|
-
return this._state;
|
|
17249
|
-
}
|
|
17250
|
-
/** When the activity entered `running` (undefined before first start). */
|
|
17251
|
-
get startedAt() {
|
|
17252
|
-
return this._startedAt;
|
|
17253
|
-
}
|
|
17254
|
-
/** Internal marker used by the registry on registration. */
|
|
17255
|
-
markRegistered() {
|
|
17256
|
-
this._state = "registered";
|
|
17257
|
-
}
|
|
17258
|
-
async initialize(context) {
|
|
17259
|
-
this._state = "initialized";
|
|
17260
|
-
publishEvent(context, ACTIVITY_EVENTS.INITIALIZED, this.descriptor, {
|
|
17261
|
-
activityId: this.descriptor.id,
|
|
17262
|
-
provider: this.descriptor.provider
|
|
17269
|
+
io.on("connection", (socket) => {
|
|
17270
|
+
let registeredStudentId = null;
|
|
17271
|
+
socket.on("register-student", (data) => {
|
|
17272
|
+
registeredStudentId = data.studentId;
|
|
17273
|
+
onlineStudents.set(data.studentId, { socketId: socket.id, name: data.name });
|
|
17274
|
+
console.log(`[Presence] Student online: ${data.name} (${data.studentId})`);
|
|
17275
|
+
broadcastPresence();
|
|
17263
17276
|
});
|
|
17264
|
-
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
payload ?? {},
|
|
17276
|
-
this.descriptor.provider,
|
|
17277
|
-
{ activityId: this.descriptor.id }
|
|
17278
|
-
);
|
|
17279
|
-
try {
|
|
17280
|
-
result = await context.commandBus.execute(command);
|
|
17281
|
-
} catch (err) {
|
|
17282
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
17283
|
-
if (!/No handler registered for command/.test(message)) {
|
|
17284
|
-
throw err;
|
|
17285
|
-
}
|
|
17277
|
+
socket.on("enter-lesson", (data) => {
|
|
17278
|
+
activeStudentLessons.set(data.studentId, data.lessonId);
|
|
17279
|
+
socket.join(data.lessonId);
|
|
17280
|
+
console.log(`[Presence] Student ${data.studentId} entered lesson ${data.lessonId}`);
|
|
17281
|
+
broadcastPresence();
|
|
17282
|
+
const activeSeg = lessonActiveSegments.get(data.lessonId);
|
|
17283
|
+
if (activeSeg) {
|
|
17284
|
+
socket.emit("student-active-segment-changed", {
|
|
17285
|
+
lessonId: data.lessonId,
|
|
17286
|
+
activeSegmentId: activeSeg
|
|
17287
|
+
});
|
|
17286
17288
|
}
|
|
17287
|
-
}
|
|
17288
|
-
this._state = "running";
|
|
17289
|
-
this._startedAt = Date.now();
|
|
17290
|
-
publishEvent(context, ACTIVITY_EVENTS.STARTED, this.descriptor, {
|
|
17291
|
-
activityId: this.descriptor.id,
|
|
17292
|
-
provider: this.descriptor.provider,
|
|
17293
|
-
commandType: this.descriptor.commandType,
|
|
17294
|
-
payload
|
|
17295
17289
|
});
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
|
|
17299
|
-
|
|
17300
|
-
|
|
17301
|
-
|
|
17290
|
+
socket.on("leave-lesson", (data) => {
|
|
17291
|
+
const oldRoom = activeStudentLessons.get(data.studentId);
|
|
17292
|
+
if (oldRoom) {
|
|
17293
|
+
socket.leave(oldRoom);
|
|
17294
|
+
}
|
|
17295
|
+
activeStudentLessons.delete(data.studentId);
|
|
17296
|
+
console.log(`[Presence] Student ${data.studentId} left lesson`);
|
|
17297
|
+
broadcastPresence();
|
|
17302
17298
|
});
|
|
17303
|
-
|
|
17304
|
-
|
|
17305
|
-
}
|
|
17306
|
-
}
|
|
17307
|
-
async resume(context) {
|
|
17308
|
-
this._state = "running";
|
|
17309
|
-
publishEvent(context, ACTIVITY_EVENTS.RESUMED, this.descriptor, {
|
|
17310
|
-
activityId: this.descriptor.id
|
|
17299
|
+
socket.on("join-room", (roomId) => {
|
|
17300
|
+
socket.join(roomId);
|
|
17311
17301
|
});
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
}
|
|
17315
|
-
}
|
|
17316
|
-
async finish(context) {
|
|
17317
|
-
this._state = "finished";
|
|
17318
|
-
publishEvent(context, ACTIVITY_EVENTS.FINISHED, this.descriptor, {
|
|
17319
|
-
activityId: this.descriptor.id
|
|
17302
|
+
socket.on("whiteboard-update", (data) => {
|
|
17303
|
+
socket.to(data.roomId).emit("whiteboard-sync", data);
|
|
17320
17304
|
});
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17305
|
+
socket.on(
|
|
17306
|
+
"whiteboard-event",
|
|
17307
|
+
(data) => {
|
|
17308
|
+
eventBus.publish({
|
|
17309
|
+
id: data.id,
|
|
17310
|
+
type: data.type,
|
|
17311
|
+
source: "whiteboard",
|
|
17312
|
+
payload: data.payload,
|
|
17313
|
+
timestamp: data.timestamp,
|
|
17314
|
+
correlationId: data.payload.lessonId
|
|
17315
|
+
});
|
|
17316
|
+
const lessonId = data.payload.lessonId;
|
|
17317
|
+
if (lessonId) {
|
|
17318
|
+
const roomName = lessonId.startsWith("assignment-") ? lessonId : `lesson-${lessonId}`;
|
|
17319
|
+
socket.to(data.payload.lessonId).emit("whiteboard-sync", {
|
|
17320
|
+
type: "refresh",
|
|
17321
|
+
sourceEvent: data.type
|
|
17322
|
+
});
|
|
17323
|
+
}
|
|
17324
|
+
}
|
|
17325
|
+
);
|
|
17326
|
+
socket.on("teacher-broadcast-segment", (data) => {
|
|
17327
|
+
lessonActiveSegments.set(data.lessonId, data.activeSegmentId);
|
|
17328
|
+
io.to(data.lessonId).emit("student-active-segment-changed", data);
|
|
17329
17329
|
});
|
|
17330
|
-
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17334
|
-
|
|
17330
|
+
socket.on("teacher-ping-student", (data) => {
|
|
17331
|
+
console.log(`[Ping] Teacher pinged student ${data.studentId} for lesson ${data.lessonId}`);
|
|
17332
|
+
const studentOnlineInfo = onlineStudents.get(data.studentId);
|
|
17333
|
+
if (studentOnlineInfo) {
|
|
17334
|
+
io.to(studentOnlineInfo.socketId).emit("student-pinged", {
|
|
17335
|
+
lessonId: data.lessonId,
|
|
17336
|
+
message: data.message
|
|
17337
|
+
});
|
|
17338
|
+
}
|
|
17339
|
+
});
|
|
17340
|
+
socket.on("disconnect", () => {
|
|
17341
|
+
if (registeredStudentId) {
|
|
17342
|
+
onlineStudents.delete(registeredStudentId);
|
|
17343
|
+
activeStudentLessons.delete(registeredStudentId);
|
|
17344
|
+
console.log(`[Presence] Student offline: ${registeredStudentId}`);
|
|
17345
|
+
broadcastPresence();
|
|
17346
|
+
}
|
|
17347
|
+
});
|
|
17348
|
+
socket.emit("presence-update", {
|
|
17349
|
+
onlineStudentIds: Array.from(onlineStudents.keys()),
|
|
17350
|
+
activeStudentLessons: Object.fromEntries(activeStudentLessons.entries())
|
|
17351
|
+
});
|
|
17352
|
+
});
|
|
17353
|
+
}
|
|
17335
17354
|
|
|
17336
|
-
//
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
* Official and plugin providers go through this identical method.
|
|
17344
|
-
*/
|
|
17345
|
-
registerProvider(provider) {
|
|
17346
|
-
if (!provider || !provider.descriptor || !provider.descriptor.id) {
|
|
17347
|
-
throw new Error("ActivityRegistry: an ActivityProvider must expose a descriptor with a valid id.");
|
|
17348
|
-
}
|
|
17349
|
-
const id = provider.descriptor.id;
|
|
17350
|
-
if (this.providers.has(id)) {
|
|
17351
|
-
throw new Error(`ActivityRegistry: activity provider "${id}" is already registered.`);
|
|
17355
|
+
// server/bootstrap-db.ts
|
|
17356
|
+
async function runStartupMigrations(db2) {
|
|
17357
|
+
try {
|
|
17358
|
+
const existingQuiz = db2.prepare("SELECT id, manifest, source_code FROM plugins WHERE name = ?").get("Quiz Component Plugin");
|
|
17359
|
+
if (existingQuiz && (!existingQuiz.manifest || !existingQuiz.manifest.includes("classroomTools") || !existingQuiz.source_code.includes("actorId:"))) {
|
|
17360
|
+
console.log("Upgrading old Quiz Component Plugin to add classroomTools and fix Actor...");
|
|
17361
|
+
db2.prepare("DELETE FROM plugins WHERE id = ?").run(existingQuiz.id);
|
|
17352
17362
|
}
|
|
17353
|
-
|
|
17354
|
-
if (
|
|
17355
|
-
|
|
17363
|
+
const existingRollCall = db2.prepare("SELECT id, manifest FROM plugins WHERE name = ?").get("Random Student Picker (\u968F\u673A\u70B9\u540D\u5C0F\u5DE5\u5177)");
|
|
17364
|
+
if (existingRollCall && (!existingRollCall.manifest || !existingRollCall.manifest.includes("classroomTools"))) {
|
|
17365
|
+
console.log("Upgrading old Random Student Picker Plugin to add classroomTools...");
|
|
17366
|
+
db2.prepare("DELETE FROM plugins WHERE id = ?").run(existingRollCall.id);
|
|
17356
17367
|
}
|
|
17368
|
+
} catch (e) {
|
|
17369
|
+
console.error("Error upgrading old default plugins:", e);
|
|
17357
17370
|
}
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17371
|
+
try {
|
|
17372
|
+
db2.exec(`
|
|
17373
|
+
CREATE TABLE IF NOT EXISTS student_rollcalls (
|
|
17374
|
+
id TEXT PRIMARY KEY,
|
|
17375
|
+
student_id TEXT NOT NULL,
|
|
17376
|
+
class_id TEXT,
|
|
17377
|
+
lesson_id TEXT,
|
|
17378
|
+
picked_time INTEGER NOT NULL
|
|
17379
|
+
);
|
|
17380
|
+
`);
|
|
17381
|
+
console.log("student_rollcalls table successfully ensured.");
|
|
17382
|
+
} catch (e) {
|
|
17383
|
+
console.error("Error creating student_rollcalls table:", e);
|
|
17367
17384
|
}
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17385
|
+
try {
|
|
17386
|
+
db2.exec(`
|
|
17387
|
+
CREATE TABLE IF NOT EXISTS site_settings (
|
|
17388
|
+
id TEXT PRIMARY KEY,
|
|
17389
|
+
site_name TEXT,
|
|
17390
|
+
slogan TEXT,
|
|
17391
|
+
logo_url TEXT
|
|
17392
|
+
);
|
|
17393
|
+
`);
|
|
17394
|
+
console.log("site_settings table successfully ensured.");
|
|
17395
|
+
} catch (e) {
|
|
17396
|
+
console.error("Error creating site_settings table:", e);
|
|
17371
17397
|
}
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17398
|
+
try {
|
|
17399
|
+
db2.exec(`
|
|
17400
|
+
CREATE TABLE IF NOT EXISTS agent_conversations (
|
|
17401
|
+
id TEXT PRIMARY KEY,
|
|
17402
|
+
conv_key TEXT NOT NULL,
|
|
17403
|
+
role TEXT NOT NULL,
|
|
17404
|
+
content TEXT NOT NULL,
|
|
17405
|
+
created_at INTEGER NOT NULL
|
|
17406
|
+
);
|
|
17407
|
+
`);
|
|
17408
|
+
db2.exec(
|
|
17409
|
+
`CREATE INDEX IF NOT EXISTS idx_agent_conv_key ON agent_conversations(conv_key, created_at);`
|
|
17376
17410
|
);
|
|
17411
|
+
console.log("agent_conversations table successfully ensured.");
|
|
17412
|
+
} catch (e) {
|
|
17413
|
+
console.error("Error creating agent_conversations table:", e);
|
|
17377
17414
|
}
|
|
17378
|
-
|
|
17379
|
-
|
|
17415
|
+
try {
|
|
17416
|
+
db2.exec(`ALTER TABLE client_sessions ADD COLUMN expires_at INTEGER`);
|
|
17417
|
+
console.log("client_sessions.expires_at column ensured.");
|
|
17418
|
+
} catch {
|
|
17380
17419
|
}
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
if (!provider) {
|
|
17394
|
-
throw new Error(`ActivityRegistry: activity provider "${id}" not found.`);
|
|
17395
|
-
}
|
|
17396
|
-
const required = provider.descriptor.permissions ?? [];
|
|
17397
|
-
if (required.length > 0 && actorId) {
|
|
17398
|
-
const results = await Promise.all(
|
|
17399
|
-
required.map((cap) => context.capability.check(actorId, cap))
|
|
17400
|
-
);
|
|
17401
|
-
const granted = results.some(Boolean);
|
|
17402
|
-
if (!granted) {
|
|
17403
|
-
const err = new Error(
|
|
17404
|
-
`[ActivityPermission] Actor "${actorId}" is missing a required permission (${required.join(", ")}) for activity "${id}".`
|
|
17405
|
-
);
|
|
17406
|
-
err.code = "PERMISSION_DENIED";
|
|
17407
|
-
throw err;
|
|
17408
|
-
}
|
|
17420
|
+
try {
|
|
17421
|
+
const now = Date.now();
|
|
17422
|
+
const idleTimeout = 24 * 60 * 60 * 1e3;
|
|
17423
|
+
const deletedExpired = db2.prepare(
|
|
17424
|
+
"DELETE FROM client_sessions WHERE expires_at IS NOT NULL AND expires_at < ?"
|
|
17425
|
+
).run(now);
|
|
17426
|
+
const deletedIdle = db2.prepare(
|
|
17427
|
+
"DELETE FROM client_sessions WHERE updated_at IS NOT NULL AND (? - updated_at) > ?"
|
|
17428
|
+
).run(now, idleTimeout);
|
|
17429
|
+
const totalDeleted = (deletedExpired.changes || 0) + (deletedIdle.changes || 0);
|
|
17430
|
+
if (totalDeleted > 0) {
|
|
17431
|
+
console.log(`[Session] Cleaned up ${totalDeleted} expired sessions on startup.`);
|
|
17409
17432
|
}
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
provider: provider.descriptor.provider,
|
|
17413
|
-
dispatched: Boolean(provider.descriptor.commandType),
|
|
17414
|
-
result
|
|
17415
|
-
};
|
|
17433
|
+
} catch (e) {
|
|
17434
|
+
console.warn("[Session] Could not clean up expired sessions:", e);
|
|
17416
17435
|
}
|
|
17417
|
-
|
|
17418
|
-
|
|
17436
|
+
}
|
|
17437
|
+
|
|
17438
|
+
// server/ai-agent.ts
|
|
17439
|
+
var import_genai2 = require("@google/genai");
|
|
17440
|
+
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
17441
|
+
var buildAgentSystemInstruction = (lang, currentLessonId) => {
|
|
17442
|
+
let systemInstruction = lang === "zh" ? "\u4F60\u662F\u4E00\u4E2A\u6559\u80B2\u7CFB\u7EDF\u5E95\u5C42\u7684 OS Agent\u3002\u4F60\u9700\u8981\u7406\u89E3\u8001\u5E08\u7684\u6307\u4EE4\uFF0C\u5E76\u8C03\u7528\u53EF\u7528\u7684\u5DE5\u5177\uFF08\u547D\u4EE4\uFF09\u53BB\u6267\u884C\u8FD9\u4E9B\u64CD\u4F5C\u3002\u5982\u679C\u8001\u5E08\u8BA9\u4F60\u521B\u5EFA\u4E00\u8282\u8BFE\uFF0C\u8BF7\u52A1\u5FC5\u5229\u7528\u5DE5\u5177\u751F\u6210\u8BE6\u7EC6\u7684\u521D\u59CB\u8BFE\u7A0B\u5185\u5BB9\u3002\u5982\u679C\u8001\u5E08\u8981\u6C42\u7BA1\u7406\u8FDB\u7A0B/\u4EFB\u52A1\uFF0C\u8BF7\u4F7F\u7528 process.spawn, process.kill, process.list\u3002\u5982\u679C\u9700\u5B58\u50A8\u6587\u4EF6\u3001\u7D20\u6750\u6216\u521B\u5EFA\u76EE\u5F55\uFF0C\u8BF7\u4F7F\u7528 vfs.* \u5E76\u5728\u9700\u8981\u65F6\u7BA1\u7406\u73ED\u7EA7\u548C\u5B66\u751F\u3002\u4F60\u652F\u6301\u901A\u8FC7 class_create \u521B\u5EFA\u73ED\u7EA7, student_create \u521B\u5EFA\u5B66\u751F, class_add_student \u5C06\u5B66\u751F\u52A0\u5165\u73ED\u7EA7\u3002\u5F53\u8001\u5E08\u8981\u6C42\u4ECE\u63D0\u4F9B\u7684\u6570\u636E\uFF08\u5982CSV\u3001JSON\u3001Markdown\u6216\u5BF9\u8BDD\u4E2D\uFF09\u521B\u5EFA\u73ED\u7EA7\u6216\u5B66\u751F\u65F6\uFF0C\u8BF7\u4F9D\u6B21\u53D1\u51FA\u8FD9\u4E9B\u6307\u4EE4\u3002\u5982\u679C\u4E0A\u4E00\u9636\u6BB5\u8FD4\u56DE\u4E86\u521B\u5EFA\u6210\u529F\u7684\u73ED\u7EA7ID\u6216\u5B66\u751FID\uFF0C\u4F60\u9700\u8981\u5728\u540E\u7EED\uFFFD? functionCall \u4E2D\u5F15\u7528\u8FD9\u4E9BID\uFF08\u4F8B\u5982\uFF1A\u628A\u521A\u521B\u5EFA\u7684\u5B66\u751FID\u52A0\u5165\u5230\u521A\u521B\u5EFA\u7684\u73ED\u7EA7ID\u4E2D\uFF09\u3002\u901A\u8FC7\u5F80\u590D\u7684\u5DE5\u5177\u8C03\u7528\uFF0C\u4F60\u53EF\u4EE5\u81EA\u52A8\u5B8C\u6210\u5B8C\u6574\u7684\u6D41\u7A0B\uFFFD?" : "You are an educational OS kernel agent. You interpret teacher instructions and use your available tools (commands) to execute them. If the teacher asks to create a lesson, always generate some detailed initial content for it. If the teacher asks to spawn or kill processes, use process tools. Use vfs tools to store assets, and manage classes/students as necessary. You support class_create, student_create, class_add_student. Always use tool chaining if you need to create a class and enroll students: first call class_create/student_create, receive their returned IDs, and then call class_add_student in the next turn. Always answer with a helpful summary.";
|
|
17443
|
+
if (currentLessonId) {
|
|
17444
|
+
systemInstruction += `
|
|
17445
|
+
[Context] The current selected lesson ID is "${currentLessonId}". Use this ID if the teacher's instruction is about modifying or adding to the current lesson.
|
|
17446
|
+
|
|
17447
|
+
Available tools (functions) can be used multiple times in sequence if needed.`;
|
|
17419
17448
|
}
|
|
17449
|
+
return systemInstruction;
|
|
17420
17450
|
};
|
|
17451
|
+
var buildAgentFinalMessage = (message, attachments) => {
|
|
17452
|
+
let finalMessage = message;
|
|
17453
|
+
if (attachments && Array.isArray(attachments) && attachments.length > 0) {
|
|
17454
|
+
finalMessage += "\n\n[Attached Reference Files]";
|
|
17455
|
+
attachments.forEach((file, index) => {
|
|
17456
|
+
if (file.name.endsWith(".zip") || file.content.startsWith("data:application/zip") || file.content.length > 5e3) {
|
|
17457
|
+
finalMessage += `
|
|
17421
17458
|
|
|
17422
|
-
|
|
17423
|
-
|
|
17424
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
payload: {
|
|
17433
|
-
type: "OBJECT",
|
|
17434
|
-
description: `Optional configuration for the ${d.name} activity`
|
|
17435
|
-
}
|
|
17459
|
+
Filename: "${file.name}"
|
|
17460
|
+
Content: "ATTACHMENT_BASE64:${index}"`;
|
|
17461
|
+
} else {
|
|
17462
|
+
finalMessage += `
|
|
17463
|
+
|
|
17464
|
+
Filename: "${file.name}"
|
|
17465
|
+
Content:
|
|
17466
|
+
"""
|
|
17467
|
+
${file.content}
|
|
17468
|
+
"""`;
|
|
17436
17469
|
}
|
|
17437
|
-
}
|
|
17438
|
-
capabilityRequired: d.permissions?.[0] ?? "lesson:read"
|
|
17439
|
-
};
|
|
17440
|
-
}
|
|
17441
|
-
var OFFICIAL_ACTIVITY_DEFINITIONS = [
|
|
17442
|
-
{
|
|
17443
|
-
id: "official_quiz",
|
|
17444
|
-
name: "Quiz",
|
|
17445
|
-
description: "In-class multiple choice / graded quiz.",
|
|
17446
|
-
icon: "HelpCircle",
|
|
17447
|
-
category: "assessment",
|
|
17448
|
-
permissions: ["quiz:write"],
|
|
17449
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17450
|
-
supportedDevices: ["desktop", "tablet", "all"],
|
|
17451
|
-
tags: ["assessment", "quiz"],
|
|
17452
|
-
version: "1.0.0",
|
|
17453
|
-
provider: OFFICIAL_PROVIDER,
|
|
17454
|
-
commandType: "quiz.create"
|
|
17455
|
-
},
|
|
17456
|
-
{
|
|
17457
|
-
id: "official_vote",
|
|
17458
|
-
name: "Vote",
|
|
17459
|
-
description: "Quick classroom vote / poll on a question.",
|
|
17460
|
-
icon: "Vote",
|
|
17461
|
-
category: "engagement",
|
|
17462
|
-
permissions: ["lesson:control"],
|
|
17463
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17464
|
-
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
17465
|
-
tags: ["engagement", "vote", "poll"],
|
|
17466
|
-
version: "1.0.0",
|
|
17467
|
-
provider: OFFICIAL_PROVIDER,
|
|
17468
|
-
commandType: "vote.create"
|
|
17469
|
-
},
|
|
17470
|
-
{
|
|
17471
|
-
id: "official_poll",
|
|
17472
|
-
name: "Poll",
|
|
17473
|
-
description: "Live polling with instant results.",
|
|
17474
|
-
icon: "BarChart3",
|
|
17475
|
-
category: "engagement",
|
|
17476
|
-
permissions: ["lesson:control"],
|
|
17477
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17478
|
-
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
17479
|
-
tags: ["engagement", "poll"],
|
|
17480
|
-
version: "1.0.0",
|
|
17481
|
-
provider: OFFICIAL_PROVIDER,
|
|
17482
|
-
commandType: "poll.create"
|
|
17483
|
-
},
|
|
17484
|
-
{
|
|
17485
|
-
id: "official_discussion",
|
|
17486
|
-
name: "Discussion",
|
|
17487
|
-
description: "Whole-class or group real-time discussion thread.",
|
|
17488
|
-
icon: "MessagesSquare",
|
|
17489
|
-
category: "collaboration",
|
|
17490
|
-
permissions: ["lesson:control"],
|
|
17491
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17492
|
-
supportedDevices: ["desktop", "tablet", "all"],
|
|
17493
|
-
tags: ["collaboration", "discussion"],
|
|
17494
|
-
version: "1.0.0",
|
|
17495
|
-
provider: OFFICIAL_PROVIDER,
|
|
17496
|
-
commandType: "discussion.create"
|
|
17497
|
-
},
|
|
17498
|
-
{
|
|
17499
|
-
id: "official_grouping",
|
|
17500
|
-
name: "Grouping",
|
|
17501
|
-
description: "Auto / random / manual student grouping.",
|
|
17502
|
-
icon: "Users",
|
|
17503
|
-
category: "collaboration",
|
|
17504
|
-
permissions: ["lesson:control"],
|
|
17505
|
-
supportedRoles: ["teacher"],
|
|
17506
|
-
supportedDevices: ["desktop", "all"],
|
|
17507
|
-
tags: ["collaboration", "group"],
|
|
17508
|
-
version: "1.0.0",
|
|
17509
|
-
provider: OFFICIAL_PROVIDER,
|
|
17510
|
-
commandType: "grouping.create"
|
|
17511
|
-
},
|
|
17512
|
-
{
|
|
17513
|
-
id: "official_assignment",
|
|
17514
|
-
name: "Assignment",
|
|
17515
|
-
description: "Create, submit and grade student assignments / homework.",
|
|
17516
|
-
icon: "FileText",
|
|
17517
|
-
category: "management",
|
|
17518
|
-
permissions: ["management:write"],
|
|
17519
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17520
|
-
supportedDevices: ["desktop", "tablet", "all"],
|
|
17521
|
-
tags: ["management", "assignment", "homework"],
|
|
17522
|
-
version: "1.0.0",
|
|
17523
|
-
provider: OFFICIAL_PROVIDER,
|
|
17524
|
-
commandType: "assignment.create"
|
|
17525
|
-
},
|
|
17526
|
-
{
|
|
17527
|
-
id: "official_competition",
|
|
17528
|
-
name: "Competition",
|
|
17529
|
-
description: "Timed leaderboard competition between students / groups.",
|
|
17530
|
-
icon: "Trophy",
|
|
17531
|
-
category: "engagement",
|
|
17532
|
-
permissions: ["lesson:control"],
|
|
17533
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17534
|
-
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
17535
|
-
tags: ["engagement", "competition", "gamification"],
|
|
17536
|
-
version: "1.0.0",
|
|
17537
|
-
provider: OFFICIAL_PROVIDER,
|
|
17538
|
-
commandType: "competition.create"
|
|
17539
|
-
},
|
|
17540
|
-
{
|
|
17541
|
-
id: "official_checkin",
|
|
17542
|
-
name: "Check-in",
|
|
17543
|
-
description: "Take class attendance / check-in.",
|
|
17544
|
-
icon: "CheckCircle2",
|
|
17545
|
-
category: "management",
|
|
17546
|
-
permissions: ["management:write"],
|
|
17547
|
-
supportedRoles: ["teacher"],
|
|
17548
|
-
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
17549
|
-
tags: ["management", "attendance", "checkin"],
|
|
17550
|
-
version: "1.0.0",
|
|
17551
|
-
provider: OFFICIAL_PROVIDER,
|
|
17552
|
-
commandType: "attendance.record"
|
|
17553
|
-
},
|
|
17554
|
-
{
|
|
17555
|
-
id: "official_homework",
|
|
17556
|
-
name: "Homework",
|
|
17557
|
-
description: "Assign and track out-of-class homework.",
|
|
17558
|
-
icon: "BookOpen",
|
|
17559
|
-
category: "management",
|
|
17560
|
-
permissions: ["management:write"],
|
|
17561
|
-
supportedRoles: ["teacher", "student", "all"],
|
|
17562
|
-
supportedDevices: ["desktop", "tablet", "all"],
|
|
17563
|
-
tags: ["management", "homework", "assignment"],
|
|
17564
|
-
version: "1.0.0",
|
|
17565
|
-
provider: OFFICIAL_PROVIDER,
|
|
17566
|
-
commandType: "assignment.create"
|
|
17470
|
+
});
|
|
17567
17471
|
}
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
17574
|
-
|
|
17575
|
-
|
|
17576
|
-
|
|
17577
|
-
|
|
17472
|
+
return finalMessage;
|
|
17473
|
+
};
|
|
17474
|
+
var normalizeToolSchema = (schema) => {
|
|
17475
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
17476
|
+
if (Array.isArray(schema)) return schema.map(normalizeToolSchema);
|
|
17477
|
+
const normalized = {};
|
|
17478
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
17479
|
+
if (key === "type" && typeof value === "string") {
|
|
17480
|
+
const typeMap = {
|
|
17481
|
+
OBJECT: "object",
|
|
17482
|
+
STRING: "string",
|
|
17483
|
+
ARRAY: "array",
|
|
17484
|
+
INTEGER: "integer",
|
|
17485
|
+
NUMBER: "number",
|
|
17486
|
+
BOOLEAN: "boolean"
|
|
17487
|
+
};
|
|
17488
|
+
normalized.type = typeMap[value.toUpperCase()] || value.toLowerCase();
|
|
17489
|
+
continue;
|
|
17490
|
+
}
|
|
17491
|
+
if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
17492
|
+
normalized.properties = Object.fromEntries(
|
|
17493
|
+
Object.entries(value).map(([propKey, propSchema]) => [propKey, normalizeToolSchema(propSchema)])
|
|
17494
|
+
);
|
|
17495
|
+
continue;
|
|
17496
|
+
}
|
|
17497
|
+
if (key === "items") {
|
|
17498
|
+
normalized.items = normalizeToolSchema(value);
|
|
17499
|
+
continue;
|
|
17578
17500
|
}
|
|
17501
|
+
normalized[key] = value;
|
|
17579
17502
|
}
|
|
17580
|
-
|
|
17581
|
-
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
|
|
17586
|
-
|
|
17587
|
-
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
|
|
17605
|
-
|
|
17606
|
-
}
|
|
17607
|
-
if (import_fs7.default.existsSync(envPath)) {
|
|
17503
|
+
return normalized;
|
|
17504
|
+
};
|
|
17505
|
+
var buildOpenAITools = () => {
|
|
17506
|
+
const actions = kernelContainer.actionRegistry.getAllActions();
|
|
17507
|
+
return actions.map((action) => ({
|
|
17508
|
+
type: "function",
|
|
17509
|
+
function: {
|
|
17510
|
+
name: action.commandType.replace(/[^a-zA-Z0-9_\-]/g, "_"),
|
|
17511
|
+
description: action.description,
|
|
17512
|
+
parameters: normalizeToolSchema(action.inputSchema)
|
|
17513
|
+
}
|
|
17514
|
+
}));
|
|
17515
|
+
};
|
|
17516
|
+
var executeAgentToolCall = async (toolName, args, allExecutedTools, callerRole, currentLessonId) => {
|
|
17517
|
+
const actionDesc = kernelContainer.actionRegistry.getActionByToolName(toolName);
|
|
17518
|
+
let actionResult;
|
|
17519
|
+
const isAdmin = callerRole === "administrator";
|
|
17520
|
+
const actorId = isAdmin ? "user-frontend" : "agent-system-0";
|
|
17521
|
+
const metadata = isAdmin ? { approved: true } : void 0;
|
|
17522
|
+
if (actionDesc) {
|
|
17523
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
17524
|
+
actionDesc.commandType,
|
|
17525
|
+
args,
|
|
17526
|
+
actorId,
|
|
17527
|
+
metadata
|
|
17528
|
+
);
|
|
17608
17529
|
try {
|
|
17609
|
-
const
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17530
|
+
const cmdResult = await kernelContainer.commandBus.execute(cmd);
|
|
17531
|
+
actionResult = cmdResult;
|
|
17532
|
+
allExecutedTools.push({ callName: toolName, success: true, result: cmdResult });
|
|
17533
|
+
if (cmdResult && cmdResult.elementId && currentLessonId) {
|
|
17534
|
+
const activeSeg = lessonActiveSegments.get(currentLessonId);
|
|
17535
|
+
if (activeSeg) {
|
|
17536
|
+
const row = kernelContainer.db.prepare("SELECT data FROM whiteboard_elements WHERE id = ?").get(cmdResult.elementId);
|
|
17537
|
+
if (row) {
|
|
17538
|
+
try {
|
|
17539
|
+
const dataObj = JSON.parse(row.data);
|
|
17540
|
+
if (!dataObj.segmentId) {
|
|
17541
|
+
dataObj.segmentId = activeSeg;
|
|
17542
|
+
kernelContainer.db.prepare("UPDATE whiteboard_elements SET data = ? WHERE id = ?").run(JSON.stringify(dataObj), cmdResult.elementId);
|
|
17543
|
+
console.log(`[Agent Tool Sync] Injected active segment "${activeSeg}" into element "${cmdResult.elementId}"`);
|
|
17544
|
+
kernelContainer.eventBus.publish({
|
|
17545
|
+
id: import_crypto4.default.randomUUID(),
|
|
17546
|
+
type: "whiteboard.element_updated",
|
|
17547
|
+
source: "agent-tool-sync",
|
|
17548
|
+
payload: { elementId: cmdResult.elementId, lessonId: currentLessonId },
|
|
17549
|
+
timestamp: Date.now(),
|
|
17550
|
+
correlationId: cmd.id
|
|
17551
|
+
}).catch((e) => console.error("[Agent Tool Sync] Failed to publish element_updated event:", e));
|
|
17552
|
+
}
|
|
17553
|
+
} catch (e) {
|
|
17554
|
+
console.error("[Agent Tool Sync] Failed to parse/update element data:", e);
|
|
17555
|
+
}
|
|
17556
|
+
}
|
|
17557
|
+
}
|
|
17617
17558
|
}
|
|
17618
|
-
} catch {
|
|
17559
|
+
} catch (err) {
|
|
17560
|
+
actionResult = { error: err.message };
|
|
17561
|
+
allExecutedTools.push({ callName: toolName, success: false, error: err.message });
|
|
17619
17562
|
}
|
|
17563
|
+
} else {
|
|
17564
|
+
actionResult = { error: `Command / Tool not found: ${toolName}` };
|
|
17565
|
+
allExecutedTools.push({ callName: toolName, success: false, error: "Command not registered" });
|
|
17620
17566
|
}
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17625
|
-
|
|
17626
|
-
|
|
17627
|
-
import_fs7.default.writeFileSync(envPath, content);
|
|
17628
|
-
console.log("[Crypto] ENCRYPTION_KEY replaced in .env (was empty or invalid)");
|
|
17629
|
-
} else {
|
|
17630
|
-
import_fs7.default.appendFileSync(envPath, `
|
|
17631
|
-
ENCRYPTION_KEY=${newKey}
|
|
17632
|
-
`);
|
|
17633
|
-
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
17634
|
-
}
|
|
17635
|
-
} else {
|
|
17636
|
-
import_fs7.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
17637
|
-
`);
|
|
17638
|
-
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
17639
|
-
}
|
|
17640
|
-
} catch (e) {
|
|
17641
|
-
console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
|
|
17567
|
+
return actionResult;
|
|
17568
|
+
};
|
|
17569
|
+
var buildOpenAIChatUrl = (apiUrl) => {
|
|
17570
|
+
let cleanUrl = apiUrl.trim();
|
|
17571
|
+
if (!cleanUrl.endsWith("/chat/completions")) {
|
|
17572
|
+
cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
|
|
17642
17573
|
}
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
}
|
|
17647
|
-
|
|
17648
|
-
if (!
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
17653
|
-
const authTag = cipher.getAuthTag();
|
|
17654
|
-
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
17655
|
-
}
|
|
17656
|
-
function decryptApiKey(encrypted) {
|
|
17657
|
-
if (!encrypted) return "";
|
|
17658
|
-
const parts = encrypted.split(":");
|
|
17659
|
-
if (parts.length !== 3) return encrypted;
|
|
17660
|
-
try {
|
|
17661
|
-
const key = getEncryptionKey();
|
|
17662
|
-
const iv = Buffer.from(parts[0], "hex");
|
|
17663
|
-
const authTag = Buffer.from(parts[1], "hex");
|
|
17664
|
-
const ciphertext = Buffer.from(parts[2], "hex");
|
|
17665
|
-
const decipher = import_crypto4.default.createDecipheriv("aes-256-gcm", key, iv);
|
|
17666
|
-
decipher.setAuthTag(authTag);
|
|
17667
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
17668
|
-
} catch {
|
|
17669
|
-
console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
|
|
17670
|
-
return encrypted;
|
|
17574
|
+
return cleanUrl;
|
|
17575
|
+
};
|
|
17576
|
+
var runGeminiAgentChat = async (request) => {
|
|
17577
|
+
const { message, lang = "zh", currentLessonId, attachments, callerRole, history } = request;
|
|
17578
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
17579
|
+
if (!apiKey || apiKey.trim() === "" || apiKey.trim() === "MY_GEMINI_API_KEY") {
|
|
17580
|
+
throw new Error(
|
|
17581
|
+
lang === "zh" ? "\u672A\u914D\u7F6E\u53EF\u7528\u7684 AI \u670D\u52A1\u3002\u8BF7\u5728\u7BA1\u7406\u540E\u53F0\u7684\u300CAI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u4E2D\u6DFB\u52A0\u4E00\u4E2A AI \u63D0\u4F9B\u5546\uFF08\u6216\u8BBE\u7F6E GEMINI_API_KEY \u4F5C\u4E3A\u517C\u5BB9\u56DE\u9000\uFF09\u3002" : 'No AI service is configured. Please add an AI Provider in the admin dashboard\'s "AI Provider Management" (or set `GEMINI_API_KEY` as a compatible fallback).'
|
|
17582
|
+
);
|
|
17671
17583
|
}
|
|
17672
|
-
}
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
];
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17584
|
+
const ai = new import_genai2.GoogleGenAI({ apiKey: apiKey.trim() });
|
|
17585
|
+
const tools = kernelContainer.actionRegistry.getAgentTools();
|
|
17586
|
+
const systemInstruction = buildAgentSystemInstruction(lang, currentLessonId);
|
|
17587
|
+
const finalMessage = buildAgentFinalMessage(message, attachments);
|
|
17588
|
+
const historyContents = (history || []).map((h) => ({
|
|
17589
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
17590
|
+
parts: [{ text: h.content }]
|
|
17591
|
+
}));
|
|
17592
|
+
const contents = [...historyContents, { role: "user", parts: [{ text: finalMessage }] }];
|
|
17593
|
+
let loopCount = 0;
|
|
17594
|
+
const MAX_LOOPS = 5;
|
|
17595
|
+
let finalResponseText = "";
|
|
17596
|
+
const allExecutedTools = [];
|
|
17597
|
+
while (loopCount < MAX_LOOPS) {
|
|
17598
|
+
const response = await ai.models.generateContent({
|
|
17599
|
+
model: "gemini-3.5-flash",
|
|
17600
|
+
contents,
|
|
17601
|
+
config: {
|
|
17602
|
+
systemInstruction,
|
|
17603
|
+
tools,
|
|
17604
|
+
temperature: 0.1
|
|
17605
|
+
}
|
|
17606
|
+
});
|
|
17607
|
+
const candidate = response.candidates?.[0];
|
|
17608
|
+
const contentParts = candidate?.content?.parts || [];
|
|
17609
|
+
const functionCalls = contentParts.filter((p) => "functionCall" in p);
|
|
17610
|
+
if (functionCalls.length === 0) {
|
|
17611
|
+
finalResponseText = response.text || "";
|
|
17612
|
+
break;
|
|
17613
|
+
}
|
|
17614
|
+
contents.push({
|
|
17615
|
+
role: "model",
|
|
17616
|
+
parts: contentParts
|
|
17617
|
+
});
|
|
17618
|
+
const toolParts = [];
|
|
17619
|
+
for (const part of contentParts) {
|
|
17620
|
+
if ("functionCall" in part && part.functionCall) {
|
|
17621
|
+
const call = part.functionCall;
|
|
17622
|
+
if (call.args && typeof call.args === "object" && attachments) {
|
|
17623
|
+
for (const key of Object.keys(call.args)) {
|
|
17624
|
+
const val = call.args[key];
|
|
17625
|
+
if (typeof val === "string" && val.startsWith("ATTACHMENT_BASE64:")) {
|
|
17626
|
+
const idx = parseInt(val.split(":")[1]);
|
|
17627
|
+
if (attachments[idx]) {
|
|
17628
|
+
call.args[key] = attachments[idx].content;
|
|
17629
|
+
}
|
|
17630
|
+
}
|
|
17631
|
+
}
|
|
17632
|
+
}
|
|
17633
|
+
const actionResult = await executeAgentToolCall(call.name, call.args, allExecutedTools, callerRole, currentLessonId);
|
|
17634
|
+
toolParts.push({
|
|
17635
|
+
functionResponse: {
|
|
17636
|
+
name: call.name,
|
|
17637
|
+
response: typeof actionResult === "object" && actionResult !== null ? actionResult : { value: actionResult }
|
|
17638
|
+
}
|
|
17639
|
+
});
|
|
17640
|
+
}
|
|
17641
|
+
}
|
|
17642
|
+
contents.push({
|
|
17643
|
+
role: "tool",
|
|
17644
|
+
parts: toolParts
|
|
17645
|
+
});
|
|
17646
|
+
loopCount++;
|
|
17696
17647
|
}
|
|
17697
|
-
if (!
|
|
17698
|
-
|
|
17699
|
-
if (sessionRow.expires_at && sessionRow.expires_at < now) {
|
|
17700
|
-
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
17701
|
-
return null;
|
|
17648
|
+
if (loopCount >= MAX_LOOPS && !finalResponseText) {
|
|
17649
|
+
finalResponseText = "I have executed several internal commands to create or link resources, but reached the iteration limit. Please double-check the interface to confirm.";
|
|
17702
17650
|
}
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17651
|
+
return {
|
|
17652
|
+
agentText: finalResponseText,
|
|
17653
|
+
toolResults: allExecutedTools
|
|
17654
|
+
};
|
|
17655
|
+
};
|
|
17656
|
+
var runOpenAIAgentChat = async (provider, request) => {
|
|
17657
|
+
const { message, lang = "zh", currentLessonId, attachments, callerRole, history } = request;
|
|
17658
|
+
const systemInstruction = buildAgentSystemInstruction(lang, currentLessonId);
|
|
17659
|
+
const finalMessage = buildAgentFinalMessage(message, attachments);
|
|
17660
|
+
const tools = buildOpenAITools();
|
|
17661
|
+
const chatUrl = buildOpenAIChatUrl(provider.api_url);
|
|
17662
|
+
const headers = {
|
|
17663
|
+
"Content-Type": "application/json"
|
|
17664
|
+
};
|
|
17665
|
+
if (provider.api_key && provider.api_key.trim()) {
|
|
17666
|
+
headers.Authorization = `Bearer ${provider.api_key.trim()}`;
|
|
17707
17667
|
}
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
}
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
const
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
if (
|
|
17731
|
-
|
|
17668
|
+
const historyMessages = (history || []).map((h) => ({ role: h.role, content: h.content }));
|
|
17669
|
+
const messages = [
|
|
17670
|
+
{ role: "system", content: systemInstruction },
|
|
17671
|
+
...historyMessages,
|
|
17672
|
+
{ role: "user", content: finalMessage }
|
|
17673
|
+
];
|
|
17674
|
+
const allExecutedTools = [];
|
|
17675
|
+
let finalResponseText = "";
|
|
17676
|
+
const MAX_LOOPS = 5;
|
|
17677
|
+
let loopCount = 0;
|
|
17678
|
+
while (loopCount < MAX_LOOPS) {
|
|
17679
|
+
const response = await fetch(chatUrl, {
|
|
17680
|
+
method: "POST",
|
|
17681
|
+
headers,
|
|
17682
|
+
body: JSON.stringify({
|
|
17683
|
+
model: provider.model_name,
|
|
17684
|
+
messages,
|
|
17685
|
+
tools,
|
|
17686
|
+
tool_choice: tools.length > 0 ? "auto" : void 0,
|
|
17687
|
+
temperature: 0.1
|
|
17688
|
+
})
|
|
17689
|
+
});
|
|
17690
|
+
if (!response.ok) {
|
|
17691
|
+
const errorText = await response.text();
|
|
17692
|
+
throw new Error(`AI provider request failed (${response.status}): ${errorText || response.statusText}`);
|
|
17732
17693
|
}
|
|
17733
|
-
|
|
17734
|
-
|
|
17694
|
+
const data = await response.json();
|
|
17695
|
+
const assistantMessage = data.choices?.[0]?.message;
|
|
17696
|
+
if (!assistantMessage) {
|
|
17697
|
+
throw new Error("AI provider returned no assistant message");
|
|
17735
17698
|
}
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
const token = getCookieToken(req);
|
|
17743
|
-
if (!token) return false;
|
|
17744
|
-
try {
|
|
17745
|
-
const session = getValidSession(token);
|
|
17746
|
-
if (!session) return false;
|
|
17747
|
-
return session.role === "teacher" || session.role === "administrator";
|
|
17748
|
-
} catch {
|
|
17749
|
-
return false;
|
|
17750
|
-
}
|
|
17751
|
-
}
|
|
17752
|
-
|
|
17753
|
-
// server/routes/shared.ts
|
|
17754
|
-
var import_path7 = __toESM(require("path"), 1);
|
|
17755
|
-
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
17756
|
-
|
|
17757
|
-
// server/utils/bridge-sdk.ts
|
|
17758
|
-
var BRIDGE_SDK_CODE = `(function() {
|
|
17759
|
-
// Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
|
|
17760
|
-
try {
|
|
17761
|
-
Object.defineProperty(document, 'cookie', {
|
|
17762
|
-
get: function() { return ""; },
|
|
17763
|
-
set: function(val) {},
|
|
17764
|
-
configurable: true
|
|
17699
|
+
finalResponseText = typeof assistantMessage.content === "string" ? assistantMessage.content.trim() : "";
|
|
17700
|
+
const toolCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
17701
|
+
messages.push({
|
|
17702
|
+
role: "assistant",
|
|
17703
|
+
content: assistantMessage.content ?? "",
|
|
17704
|
+
tool_calls: toolCalls
|
|
17765
17705
|
});
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
}
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
const originalPostMessage = window.postMessage;
|
|
17779
|
-
window.postMessage = function(message, targetOrigin, transfer) {
|
|
17780
|
-
try {
|
|
17781
|
-
if (message && typeof message === 'object') {
|
|
17782
|
-
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
17783
|
-
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
17784
|
-
}
|
|
17785
|
-
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
17786
|
-
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
17787
|
-
}
|
|
17788
|
-
}
|
|
17789
|
-
} catch (e) {}
|
|
17790
|
-
|
|
17791
|
-
let origin = targetOrigin;
|
|
17792
|
-
if (origin === 'null') {
|
|
17793
|
-
origin = '*';
|
|
17794
|
-
}
|
|
17795
|
-
try {
|
|
17796
|
-
return originalPostMessage.call(this, message, origin, transfer);
|
|
17797
|
-
} catch (err) {
|
|
17798
|
-
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
17799
|
-
return originalPostMessage.call(this, message, '*', transfer);
|
|
17706
|
+
if (toolCalls.length === 0) {
|
|
17707
|
+
break;
|
|
17708
|
+
}
|
|
17709
|
+
for (const call of toolCalls) {
|
|
17710
|
+
const toolName = call?.function?.name;
|
|
17711
|
+
if (!toolName) continue;
|
|
17712
|
+
let parsedArgs = {};
|
|
17713
|
+
if (typeof call?.function?.arguments === "string" && call.function.arguments.trim()) {
|
|
17714
|
+
try {
|
|
17715
|
+
parsedArgs = JSON.parse(call.function.arguments);
|
|
17716
|
+
} catch (err) {
|
|
17717
|
+
parsedArgs = {};
|
|
17800
17718
|
}
|
|
17801
|
-
throw err;
|
|
17802
17719
|
}
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
if (message && typeof message === 'object') {
|
|
17811
|
-
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
17812
|
-
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
17813
|
-
}
|
|
17814
|
-
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
17815
|
-
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
17816
|
-
}
|
|
17817
|
-
}
|
|
17818
|
-
} catch (e) {}
|
|
17819
|
-
|
|
17820
|
-
let origin = targetOrigin;
|
|
17821
|
-
if (origin === 'null') {
|
|
17822
|
-
origin = '*';
|
|
17823
|
-
}
|
|
17824
|
-
try {
|
|
17825
|
-
return parentPostMessage.call(window.parent, message, origin, transfer);
|
|
17826
|
-
} catch (err) {
|
|
17827
|
-
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
17828
|
-
return parentPostMessage.call(window.parent, message, '*', transfer);
|
|
17720
|
+
if (parsedArgs && typeof parsedArgs === "object" && attachments) {
|
|
17721
|
+
for (const key of Object.keys(parsedArgs)) {
|
|
17722
|
+
const val = parsedArgs[key];
|
|
17723
|
+
if (typeof val === "string" && val.startsWith("ATTACHMENT_BASE64:")) {
|
|
17724
|
+
const idx = parseInt(val.split(":")[1]);
|
|
17725
|
+
if (attachments[idx]) {
|
|
17726
|
+
parsedArgs[key] = attachments[idx].content;
|
|
17829
17727
|
}
|
|
17830
|
-
throw err;
|
|
17831
17728
|
}
|
|
17832
|
-
}
|
|
17833
|
-
}
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
type: "LMS_SUBMIT",
|
|
17841
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17842
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17843
|
-
payload: data
|
|
17844
|
-
}, "*");
|
|
17845
|
-
},
|
|
17846
|
-
saveProgress(data) {
|
|
17847
|
-
window.parent.postMessage({
|
|
17848
|
-
type: "LMS_SAVE_PROGRESS",
|
|
17849
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17850
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17851
|
-
payload: data
|
|
17852
|
-
}, "*");
|
|
17853
|
-
},
|
|
17854
|
-
finish(data) {
|
|
17855
|
-
window.parent.postMessage({
|
|
17856
|
-
type: "LMS_FINISH",
|
|
17857
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17858
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17859
|
-
payload: data
|
|
17860
|
-
}, "*");
|
|
17861
|
-
},
|
|
17862
|
-
getStudent() {
|
|
17863
|
-
return window.__LMS_STUDENT__;
|
|
17864
|
-
},
|
|
17865
|
-
getCourseware() {
|
|
17866
|
-
return window.__LMS_COURSEWARE__;
|
|
17867
|
-
},
|
|
17868
|
-
log(event, data) {
|
|
17869
|
-
window.parent.postMessage({
|
|
17870
|
-
type: "LMS_LOG",
|
|
17871
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17872
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17873
|
-
event: event,
|
|
17874
|
-
payload: data
|
|
17875
|
-
}, "*");
|
|
17729
|
+
}
|
|
17730
|
+
}
|
|
17731
|
+
const actionResult = await executeAgentToolCall(toolName, parsedArgs, allExecutedTools, callerRole, currentLessonId);
|
|
17732
|
+
messages.push({
|
|
17733
|
+
role: "tool",
|
|
17734
|
+
tool_call_id: call.id,
|
|
17735
|
+
content: JSON.stringify(actionResult)
|
|
17736
|
+
});
|
|
17876
17737
|
}
|
|
17738
|
+
loopCount++;
|
|
17739
|
+
}
|
|
17740
|
+
if (loopCount >= MAX_LOOPS && !finalResponseText) {
|
|
17741
|
+
finalResponseText = "I have executed several internal commands, but reached the iteration limit. Please review the assistant panel for the latest state.";
|
|
17742
|
+
}
|
|
17743
|
+
return {
|
|
17744
|
+
agentText: finalResponseText,
|
|
17745
|
+
toolResults: allExecutedTools
|
|
17877
17746
|
};
|
|
17747
|
+
};
|
|
17878
17748
|
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
const url = (typeof input === 'string') ? input : (input?.url || '');
|
|
17885
|
-
const method = init?.method || input?.method || 'GET';
|
|
17886
|
-
const headers = init?.headers || input?.headers || {};
|
|
17887
|
-
let body = init?.body || input?.body || null;
|
|
17888
|
-
|
|
17889
|
-
if (body && typeof body === 'object') {
|
|
17890
|
-
try { body = JSON.stringify(body); } catch(e){}
|
|
17891
|
-
}
|
|
17892
|
-
|
|
17893
|
-
if (url && !url.includes('/api/courseware/attempts/')) {
|
|
17894
|
-
window.parent.postMessage({
|
|
17895
|
-
type: "HOOK_FETCH",
|
|
17896
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17897
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17898
|
-
payload: { url, method, headers: JSON.parse(JSON.stringify(headers)), body: body ? body.toString() : null }
|
|
17899
|
-
}, "*");
|
|
17900
|
-
}
|
|
17901
|
-
} catch (e) {
|
|
17902
|
-
console.error("Bridge Hook fetch error", e);
|
|
17903
|
-
}
|
|
17904
|
-
return originalFetch.apply(this, arguments);
|
|
17905
|
-
};
|
|
17749
|
+
// packages/core/bootstrap/adapter/bootstrap-registration.ts
|
|
17750
|
+
var BootstrapRegistration = class {
|
|
17751
|
+
static registerConfiguration(builder, context) {
|
|
17752
|
+
if (context.config) {
|
|
17753
|
+
builder.withConfiguration(context.config);
|
|
17906
17754
|
}
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
const originalOpen = XMLHttpRequest.prototype.open;
|
|
17910
|
-
const originalSend = XMLHttpRequest.prototype.send;
|
|
17911
|
-
XMLHttpRequest.prototype.open = function(method, url) {
|
|
17912
|
-
this._method = method;
|
|
17913
|
-
this._url = url;
|
|
17914
|
-
return originalOpen.apply(this, arguments);
|
|
17915
|
-
};
|
|
17916
|
-
XMLHttpRequest.prototype.send = function(body) {
|
|
17917
|
-
try {
|
|
17918
|
-
let bodyStr = body;
|
|
17919
|
-
if (body && typeof body === 'object') {
|
|
17920
|
-
try { bodyStr = JSON.stringify(body); } catch(e){}
|
|
17921
|
-
}
|
|
17922
|
-
if (this._url && !this._url.includes('/api/courseware/attempts/')) {
|
|
17923
|
-
window.parent.postMessage({
|
|
17924
|
-
type: "HOOK_XHR",
|
|
17925
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17926
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17927
|
-
payload: { url: this._url, method: this._method, body: bodyStr ? bodyStr.toString() : null }
|
|
17928
|
-
}, "*");
|
|
17929
|
-
}
|
|
17930
|
-
} catch (e) {
|
|
17931
|
-
console.error("Bridge Hook XHR error", e);
|
|
17932
|
-
}
|
|
17933
|
-
return originalSend.apply(this, arguments);
|
|
17934
|
-
};
|
|
17755
|
+
if (context.environment) {
|
|
17756
|
+
builder.withEnvironment(context.environment);
|
|
17935
17757
|
}
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
payload: { url: config.url, method: config.method, data: config.data }
|
|
17947
|
-
}, "*");
|
|
17948
|
-
}
|
|
17949
|
-
} catch (e) {
|
|
17950
|
-
console.error("Bridge Hook Axios error", e);
|
|
17951
|
-
}
|
|
17952
|
-
return config;
|
|
17953
|
-
}, function(error) { return Promise.reject(error); });
|
|
17758
|
+
}
|
|
17759
|
+
static registerLogger(builder, context) {
|
|
17760
|
+
if (context.logger) {
|
|
17761
|
+
builder.withLogger(context.logger);
|
|
17762
|
+
}
|
|
17763
|
+
}
|
|
17764
|
+
static registerInfrastructure(builder, context) {
|
|
17765
|
+
if (context.existingServices) {
|
|
17766
|
+
for (const [id, service] of context.existingServices.entries()) {
|
|
17767
|
+
builder.addService(id, service);
|
|
17954
17768
|
}
|
|
17955
17769
|
}
|
|
17956
|
-
if (
|
|
17957
|
-
|
|
17770
|
+
if (context.kernelContainer) {
|
|
17771
|
+
builder.addService("kernelContainer", context.kernelContainer);
|
|
17958
17772
|
}
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
attachToAxios(val);
|
|
17965
|
-
},
|
|
17966
|
-
configurable: true
|
|
17967
|
-
});
|
|
17968
|
-
|
|
17969
|
-
if (navigator && navigator.sendBeacon) {
|
|
17970
|
-
const originalSendBeacon = navigator.sendBeacon;
|
|
17971
|
-
navigator.sendBeacon = function(url, data) {
|
|
17972
|
-
try {
|
|
17973
|
-
if (url && !url.includes('/api/courseware/attempts/')) {
|
|
17974
|
-
window.parent.postMessage({
|
|
17975
|
-
type: "HOOK_BEACON",
|
|
17976
|
-
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
17977
|
-
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
17978
|
-
payload: { url: url, data: data ? data.toString() : null }
|
|
17979
|
-
}, "*");
|
|
17980
|
-
}
|
|
17981
|
-
} catch (e) {
|
|
17982
|
-
console.error("Bridge Hook Beacon error", e);
|
|
17983
|
-
}
|
|
17984
|
-
return originalSendBeacon.apply(this, arguments);
|
|
17985
|
-
};
|
|
17773
|
+
if (context.expressApp) {
|
|
17774
|
+
builder.addService("expressApp", context.expressApp);
|
|
17775
|
+
}
|
|
17776
|
+
if (context.httpServer) {
|
|
17777
|
+
builder.addService("httpServer", context.httpServer);
|
|
17986
17778
|
}
|
|
17779
|
+
}
|
|
17780
|
+
static registerExistingBootstrapStages(_builder) {
|
|
17781
|
+
}
|
|
17782
|
+
};
|
|
17987
17783
|
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
17784
|
+
// packages/core/bootstrap/adapter/server-bootstrap-adapter.ts
|
|
17785
|
+
var ServerBootstrapAdapter = class _ServerBootstrapAdapter {
|
|
17786
|
+
constructor(options) {
|
|
17787
|
+
this._state = "Created";
|
|
17788
|
+
this._state = "Created";
|
|
17789
|
+
this._builder = PlatformBuilder.create();
|
|
17790
|
+
if (options) {
|
|
17791
|
+
this.configure(options);
|
|
17792
|
+
}
|
|
17793
|
+
}
|
|
17794
|
+
static create(options) {
|
|
17795
|
+
return new _ServerBootstrapAdapter(options);
|
|
17796
|
+
}
|
|
17797
|
+
get state() {
|
|
17798
|
+
return this._state;
|
|
17799
|
+
}
|
|
17800
|
+
configure(context) {
|
|
17801
|
+
this._state = "Configuring";
|
|
17802
|
+
BootstrapRegistration.registerConfiguration(this._builder, context);
|
|
17803
|
+
BootstrapRegistration.registerLogger(this._builder, context);
|
|
17804
|
+
BootstrapRegistration.registerInfrastructure(this._builder, context);
|
|
17805
|
+
return this;
|
|
17806
|
+
}
|
|
17807
|
+
registerStages() {
|
|
17808
|
+
BootstrapRegistration.registerExistingBootstrapStages(this._builder);
|
|
17809
|
+
this._state = "PipelineRegistered";
|
|
17810
|
+
return this;
|
|
17811
|
+
}
|
|
17812
|
+
async runBootstrap(context) {
|
|
17813
|
+
if (this._state === "Created") {
|
|
17814
|
+
this.configure(context);
|
|
17815
|
+
}
|
|
17816
|
+
if (this._state === "Configuring") {
|
|
17817
|
+
this.registerStages();
|
|
17818
|
+
}
|
|
17819
|
+
const builderResult = this._builder.buildResult();
|
|
17820
|
+
const pipelineResult = await builderResult.pipeline.execute({
|
|
17821
|
+
startupTimestamp: Date.now(),
|
|
17822
|
+
startupOptions: {},
|
|
17823
|
+
startupStage: builderResult.platformContext.currentStage,
|
|
17824
|
+
startupToken: { token: "srv_adapter_token", isCancelled: false, cancel: () => {
|
|
17825
|
+
} },
|
|
17826
|
+
isCancelled: false,
|
|
17827
|
+
platformContext: builderResult.platformContext,
|
|
17828
|
+
config: builderResult.platformContext.config,
|
|
17829
|
+
state: "Active",
|
|
17830
|
+
currentStage: builderResult.platformContext.currentStage,
|
|
17831
|
+
startTime: Date.now(),
|
|
17832
|
+
getMetadata: () => void 0,
|
|
17833
|
+
setStage: () => {
|
|
18006
17834
|
}
|
|
18007
|
-
}
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
function logToServer(msg, detail) {
|
|
18011
|
-
// \u4EC5 console \u8F93\u51FA\uFF1B\u7F51\u7EDC\u8BF7\u6C42\u53EF\u80FD\u88AB\u6D4F\u89C8\u5668 HTTPS \u5347\u7EA7\u5BFC\u81F4 ERR_CONNECTION_REFUSED
|
|
18012
|
-
try {
|
|
18013
|
-
console.log('[LMS Debug]', msg, detail || '');
|
|
18014
|
-
} catch (e) {}
|
|
17835
|
+
});
|
|
17836
|
+
if (pipelineResult.status === "Failed") {
|
|
17837
|
+
throw pipelineResult.error || new Error(`ServerBootstrapAdapter pipeline failed at stage: ${pipelineResult.failedStage}`);
|
|
18015
17838
|
}
|
|
17839
|
+
this._state = "Bootstrapped";
|
|
17840
|
+
return { builderResult, pipelineResult };
|
|
17841
|
+
}
|
|
17842
|
+
static async bootstrap(context) {
|
|
17843
|
+
const adapter = _ServerBootstrapAdapter.create(context);
|
|
17844
|
+
return adapter.runBootstrap(context);
|
|
17845
|
+
}
|
|
17846
|
+
};
|
|
18016
17847
|
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
try {
|
|
18020
|
-
const commonVars = ['score', 'points', 'grade', 'totalScore', 'currentScore', 'userScore', 'finalScore', 'correctCount'];
|
|
18021
|
-
for (const v of commonVars) {
|
|
18022
|
-
if (typeof window[v] === 'number') {
|
|
18023
|
-
logData.push("Global var " + v + " is number: " + window[v]);
|
|
18024
|
-
return { score: window[v], log: logData };
|
|
18025
|
-
}
|
|
18026
|
-
if (typeof window[v] === 'string') {
|
|
18027
|
-
const num = parseFloat(window[v]);
|
|
18028
|
-
if (!isNaN(num)) {
|
|
18029
|
-
logData.push("Global var " + v + " is string with number: " + window[v]);
|
|
18030
|
-
return { score: num, log: logData };
|
|
18031
|
-
}
|
|
18032
|
-
}
|
|
18033
|
-
}
|
|
18034
|
-
|
|
18035
|
-
const selectors = [
|
|
18036
|
-
'#score', '#scoreDisplay', '#score-num', '#scoreDisplaySpan', '#points', '#grade',
|
|
18037
|
-
'.score', '.points', '.grade', '.score-num', '.score-value',
|
|
18038
|
-
'[id*="score" i]', '[id*="point" i]', '[id*="grade" i]', '[id*="result" i]',
|
|
18039
|
-
'[class*="score" i]', '[class*="point" i]', '[class*="grade" i]', '[class*="result" i]'
|
|
18040
|
-
];
|
|
18041
|
-
|
|
18042
|
-
for (const selector of selectors) {
|
|
18043
|
-
try {
|
|
18044
|
-
const el = document.querySelector(selector);
|
|
18045
|
-
if (el) {
|
|
18046
|
-
const text = (el.textContent || el.innerText || '').trim();
|
|
18047
|
-
if (text) {
|
|
18048
|
-
logData.push("Selector '" + selector + "' matched text: '" + text + "'");
|
|
18049
|
-
const fractionMatch = text.match(/(\\\\d+(\\\\.\\\\d+)?)\\\\s*[\\\\/|\u4E4B]\\\\s*(\\\\d+)/);
|
|
18050
|
-
if (fractionMatch) {
|
|
18051
|
-
const num = parseFloat(fractionMatch[1]);
|
|
18052
|
-
const den = parseFloat(fractionMatch[3]);
|
|
18053
|
-
if (den > 0) {
|
|
18054
|
-
const pct = (num / den) * 100;
|
|
18055
|
-
logData.push("Parsed fraction: " + num + "/" + den + " -> " + pct);
|
|
18056
|
-
return { score: pct, log: logData };
|
|
18057
|
-
}
|
|
18058
|
-
}
|
|
18059
|
-
const match = text.match(/\\\\d+(\\\\.\\\\d+)?/);
|
|
18060
|
-
if (match) {
|
|
18061
|
-
const num = parseFloat(match[0]);
|
|
18062
|
-
if (!isNaN(num)) {
|
|
18063
|
-
logData.push("Parsed decimal: " + num);
|
|
18064
|
-
return { score: num, log: logData };
|
|
18065
|
-
}
|
|
18066
|
-
}
|
|
18067
|
-
}
|
|
18068
|
-
}
|
|
18069
|
-
} catch (e) {}
|
|
18070
|
-
}
|
|
17848
|
+
// packages/activity-ecosystem/index.ts
|
|
17849
|
+
init_token();
|
|
18071
17850
|
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
}
|
|
18084
|
-
}
|
|
18085
|
-
} catch (e) {}
|
|
17851
|
+
// packages/activity-ecosystem/context.ts
|
|
17852
|
+
function createActivityContext(opts) {
|
|
17853
|
+
return {
|
|
17854
|
+
commandBus: opts.commandBus,
|
|
17855
|
+
eventBus: opts.eventBus,
|
|
17856
|
+
actionRegistry: opts.actionRegistry,
|
|
17857
|
+
capability: opts.capability,
|
|
17858
|
+
ai: opts.ai,
|
|
17859
|
+
classroom: opts.classroom ?? null
|
|
17860
|
+
};
|
|
17861
|
+
}
|
|
18086
17862
|
|
|
18087
|
-
|
|
18088
|
-
|
|
17863
|
+
// packages/activity-ecosystem/provider.ts
|
|
17864
|
+
var import_uuid15 = require("uuid");
|
|
17865
|
+
var ACTIVITY_EVENTS = {
|
|
17866
|
+
REGISTERED: "activity.registered",
|
|
17867
|
+
INITIALIZED: "activity.initialized",
|
|
17868
|
+
STARTED: "activity.started",
|
|
17869
|
+
PAUSED: "activity.paused",
|
|
17870
|
+
RESUMED: "activity.resumed",
|
|
17871
|
+
FINISHED: "activity.finished",
|
|
17872
|
+
DISPOSED: "activity.disposed"
|
|
17873
|
+
};
|
|
17874
|
+
function publishEvent(context, type, descriptor, payload) {
|
|
17875
|
+
context.eventBus.publish({
|
|
17876
|
+
id: (0, import_uuid15.v7)(),
|
|
17877
|
+
type,
|
|
17878
|
+
source: `activity:${descriptor.id}`,
|
|
17879
|
+
payload,
|
|
17880
|
+
timestamp: Date.now()
|
|
17881
|
+
});
|
|
17882
|
+
}
|
|
17883
|
+
var BaseActivityProvider = class {
|
|
17884
|
+
constructor(options) {
|
|
17885
|
+
this._state = "registered";
|
|
17886
|
+
if (!options?.descriptor?.id) {
|
|
17887
|
+
throw new Error("BaseActivityProvider requires a descriptor with a valid id.");
|
|
17888
|
+
}
|
|
17889
|
+
this.descriptor = options.descriptor;
|
|
17890
|
+
this.hooks = options;
|
|
17891
|
+
}
|
|
17892
|
+
get state() {
|
|
17893
|
+
return this._state;
|
|
17894
|
+
}
|
|
17895
|
+
/** When the activity entered `running` (undefined before first start). */
|
|
17896
|
+
get startedAt() {
|
|
17897
|
+
return this._startedAt;
|
|
17898
|
+
}
|
|
17899
|
+
/** Internal marker used by the registry on registration. */
|
|
17900
|
+
markRegistered() {
|
|
17901
|
+
this._state = "registered";
|
|
17902
|
+
}
|
|
17903
|
+
async initialize(context) {
|
|
17904
|
+
this._state = "initialized";
|
|
17905
|
+
publishEvent(context, ACTIVITY_EVENTS.INITIALIZED, this.descriptor, {
|
|
17906
|
+
activityId: this.descriptor.id,
|
|
17907
|
+
provider: this.descriptor.provider
|
|
17908
|
+
});
|
|
17909
|
+
if (this.hooks.onInitialize) {
|
|
17910
|
+
await this.hooks.onInitialize(context);
|
|
17911
|
+
}
|
|
17912
|
+
}
|
|
17913
|
+
async start(context, payload) {
|
|
17914
|
+
let result;
|
|
17915
|
+
if (this.hooks.onStart) {
|
|
17916
|
+
result = await this.hooks.onStart(context, payload);
|
|
17917
|
+
} else if (this.descriptor.commandType) {
|
|
17918
|
+
const command = await context.commandBus.createCommand(
|
|
17919
|
+
this.descriptor.commandType,
|
|
17920
|
+
payload ?? {},
|
|
17921
|
+
this.descriptor.provider,
|
|
17922
|
+
{ activityId: this.descriptor.id }
|
|
17923
|
+
);
|
|
17924
|
+
try {
|
|
17925
|
+
result = await context.commandBus.execute(command);
|
|
17926
|
+
} catch (err) {
|
|
17927
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17928
|
+
if (!/No handler registered for command/.test(message)) {
|
|
17929
|
+
throw err;
|
|
17930
|
+
}
|
|
17931
|
+
}
|
|
17932
|
+
}
|
|
17933
|
+
this._state = "running";
|
|
17934
|
+
this._startedAt = Date.now();
|
|
17935
|
+
publishEvent(context, ACTIVITY_EVENTS.STARTED, this.descriptor, {
|
|
17936
|
+
activityId: this.descriptor.id,
|
|
17937
|
+
provider: this.descriptor.provider,
|
|
17938
|
+
commandType: this.descriptor.commandType,
|
|
17939
|
+
payload
|
|
17940
|
+
});
|
|
17941
|
+
return result;
|
|
17942
|
+
}
|
|
17943
|
+
async pause(context) {
|
|
17944
|
+
this._state = "paused";
|
|
17945
|
+
publishEvent(context, ACTIVITY_EVENTS.PAUSED, this.descriptor, {
|
|
17946
|
+
activityId: this.descriptor.id
|
|
17947
|
+
});
|
|
17948
|
+
if (this.hooks.onPause) {
|
|
17949
|
+
await this.hooks.onPause(context);
|
|
17950
|
+
}
|
|
17951
|
+
}
|
|
17952
|
+
async resume(context) {
|
|
17953
|
+
this._state = "running";
|
|
17954
|
+
publishEvent(context, ACTIVITY_EVENTS.RESUMED, this.descriptor, {
|
|
17955
|
+
activityId: this.descriptor.id
|
|
17956
|
+
});
|
|
17957
|
+
if (this.hooks.onResume) {
|
|
17958
|
+
await this.hooks.onResume(context);
|
|
17959
|
+
}
|
|
17960
|
+
}
|
|
17961
|
+
async finish(context) {
|
|
17962
|
+
this._state = "finished";
|
|
17963
|
+
publishEvent(context, ACTIVITY_EVENTS.FINISHED, this.descriptor, {
|
|
17964
|
+
activityId: this.descriptor.id
|
|
17965
|
+
});
|
|
17966
|
+
if (this.hooks.onFinish) {
|
|
17967
|
+
await this.hooks.onFinish(context);
|
|
17968
|
+
}
|
|
17969
|
+
}
|
|
17970
|
+
async dispose(context) {
|
|
17971
|
+
this._state = "disposed";
|
|
17972
|
+
publishEvent(context, ACTIVITY_EVENTS.DISPOSED, this.descriptor, {
|
|
17973
|
+
activityId: this.descriptor.id
|
|
17974
|
+
});
|
|
17975
|
+
if (this.hooks.onDispose) {
|
|
17976
|
+
await this.hooks.onDispose(context);
|
|
17977
|
+
}
|
|
17978
|
+
}
|
|
17979
|
+
};
|
|
17980
|
+
|
|
17981
|
+
// packages/activity-ecosystem/registry.ts
|
|
17982
|
+
var ActivityRegistry2 = class {
|
|
17983
|
+
constructor() {
|
|
17984
|
+
this.providers = /* @__PURE__ */ new Map();
|
|
17985
|
+
}
|
|
17986
|
+
/**
|
|
17987
|
+
* Register an Activity Provider. Throws on missing id or duplicate id.
|
|
17988
|
+
* Official and plugin providers go through this identical method.
|
|
17989
|
+
*/
|
|
17990
|
+
registerProvider(provider) {
|
|
17991
|
+
if (!provider || !provider.descriptor || !provider.descriptor.id) {
|
|
17992
|
+
throw new Error("ActivityRegistry: an ActivityProvider must expose a descriptor with a valid id.");
|
|
17993
|
+
}
|
|
17994
|
+
const id = provider.descriptor.id;
|
|
17995
|
+
if (this.providers.has(id)) {
|
|
17996
|
+
throw new Error(`ActivityRegistry: activity provider "${id}" is already registered.`);
|
|
17997
|
+
}
|
|
17998
|
+
this.providers.set(id, provider);
|
|
17999
|
+
if (typeof provider.markRegistered === "function") {
|
|
18000
|
+
provider.markRegistered();
|
|
18001
|
+
}
|
|
18002
|
+
}
|
|
18003
|
+
/** Remove a provider by id. Returns true if it was present. */
|
|
18004
|
+
unregisterProvider(id) {
|
|
18005
|
+
return this.providers.delete(id);
|
|
18006
|
+
}
|
|
18007
|
+
getProvider(id) {
|
|
18008
|
+
return this.providers.get(id);
|
|
18009
|
+
}
|
|
18010
|
+
listProviders() {
|
|
18011
|
+
return Array.from(this.providers.values());
|
|
18012
|
+
}
|
|
18013
|
+
/** List the declarative descriptors (for Workspace catalogue / REST). */
|
|
18014
|
+
listDescriptors() {
|
|
18015
|
+
return this.listProviders().map((p) => p.descriptor);
|
|
18016
|
+
}
|
|
18017
|
+
/** Activities visible to a given role (`all` matches everything). */
|
|
18018
|
+
listByRole(role) {
|
|
18019
|
+
return this.listProviders().filter(
|
|
18020
|
+
(p) => p.descriptor.supportedRoles.includes("all") || p.descriptor.supportedRoles.includes(role)
|
|
18021
|
+
);
|
|
18022
|
+
}
|
|
18023
|
+
listByCategory(category) {
|
|
18024
|
+
return this.listProviders().filter((p) => p.descriptor.category === category);
|
|
18025
|
+
}
|
|
18026
|
+
/**
|
|
18027
|
+
* Execute an activity's `start` lifecycle on the supplied context.
|
|
18028
|
+
*
|
|
18029
|
+
* Permission isolation: when the provider declares `permissions`, at least
|
|
18030
|
+
* one must be granted to `actorId` via the (reused) capability service,
|
|
18031
|
+
* otherwise a PERMISSION_DENIED error is thrown (403 at the REST layer).
|
|
18032
|
+
*
|
|
18033
|
+
* The activity itself reuses the Command Bus / Event Bus — this method only
|
|
18034
|
+
* orchestrates the lifecycle and enforces permission.
|
|
18035
|
+
*/
|
|
18036
|
+
async startActivity(id, context, payload, actorId) {
|
|
18037
|
+
const provider = this.providers.get(id);
|
|
18038
|
+
if (!provider) {
|
|
18039
|
+
throw new Error(`ActivityRegistry: activity provider "${id}" not found.`);
|
|
18040
|
+
}
|
|
18041
|
+
const required = provider.descriptor.permissions ?? [];
|
|
18042
|
+
if (required.length > 0 && actorId) {
|
|
18043
|
+
const results = await Promise.all(
|
|
18044
|
+
required.map((cap) => context.capability.check(actorId, cap))
|
|
18045
|
+
);
|
|
18046
|
+
const granted = results.some(Boolean);
|
|
18047
|
+
if (!granted) {
|
|
18048
|
+
const err = new Error(
|
|
18049
|
+
`[ActivityPermission] Actor "${actorId}" is missing a required permission (${required.join(", ")}) for activity "${id}".`
|
|
18050
|
+
);
|
|
18051
|
+
err.code = "PERMISSION_DENIED";
|
|
18052
|
+
throw err;
|
|
18053
|
+
}
|
|
18054
|
+
}
|
|
18055
|
+
const result = await provider.start(context, payload);
|
|
18056
|
+
return {
|
|
18057
|
+
provider: provider.descriptor.provider,
|
|
18058
|
+
dispatched: Boolean(provider.descriptor.commandType),
|
|
18059
|
+
result
|
|
18060
|
+
};
|
|
18061
|
+
}
|
|
18062
|
+
clear() {
|
|
18063
|
+
this.providers.clear();
|
|
18064
|
+
}
|
|
18065
|
+
};
|
|
18066
|
+
|
|
18067
|
+
// packages/activity-ecosystem/default-providers.ts
|
|
18068
|
+
var OFFICIAL_PROVIDER = "official";
|
|
18069
|
+
function aiActionFor(d) {
|
|
18070
|
+
return {
|
|
18071
|
+
id: `activity_ai_${d.id}`,
|
|
18072
|
+
commandType: d.commandType ?? `activity.${d.id}.start`,
|
|
18073
|
+
description: `Start the ${d.name} activity${d.description ? ` \u2014 ${d.description}` : ""}`,
|
|
18074
|
+
inputSchema: {
|
|
18075
|
+
type: "OBJECT",
|
|
18076
|
+
properties: {
|
|
18077
|
+
payload: {
|
|
18078
|
+
type: "OBJECT",
|
|
18079
|
+
description: `Optional configuration for the ${d.name} activity`
|
|
18080
|
+
}
|
|
18081
|
+
}
|
|
18082
|
+
},
|
|
18083
|
+
capabilityRequired: d.permissions?.[0] ?? "lesson:read"
|
|
18084
|
+
};
|
|
18085
|
+
}
|
|
18086
|
+
var OFFICIAL_ACTIVITY_DEFINITIONS = [
|
|
18087
|
+
{
|
|
18088
|
+
id: "official_quiz",
|
|
18089
|
+
name: "Quiz",
|
|
18090
|
+
description: "In-class multiple choice / graded quiz.",
|
|
18091
|
+
icon: "HelpCircle",
|
|
18092
|
+
category: "assessment",
|
|
18093
|
+
permissions: ["quiz:write"],
|
|
18094
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18095
|
+
supportedDevices: ["desktop", "tablet", "all"],
|
|
18096
|
+
tags: ["assessment", "quiz"],
|
|
18097
|
+
version: "1.0.0",
|
|
18098
|
+
provider: OFFICIAL_PROVIDER,
|
|
18099
|
+
commandType: "quiz.create"
|
|
18100
|
+
},
|
|
18101
|
+
{
|
|
18102
|
+
id: "official_vote",
|
|
18103
|
+
name: "Vote",
|
|
18104
|
+
description: "Quick classroom vote / poll on a question.",
|
|
18105
|
+
icon: "Vote",
|
|
18106
|
+
category: "engagement",
|
|
18107
|
+
permissions: ["lesson:control"],
|
|
18108
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18109
|
+
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
18110
|
+
tags: ["engagement", "vote", "poll"],
|
|
18111
|
+
version: "1.0.0",
|
|
18112
|
+
provider: OFFICIAL_PROVIDER,
|
|
18113
|
+
commandType: "vote.create"
|
|
18114
|
+
},
|
|
18115
|
+
{
|
|
18116
|
+
id: "official_poll",
|
|
18117
|
+
name: "Poll",
|
|
18118
|
+
description: "Live polling with instant results.",
|
|
18119
|
+
icon: "BarChart3",
|
|
18120
|
+
category: "engagement",
|
|
18121
|
+
permissions: ["lesson:control"],
|
|
18122
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18123
|
+
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
18124
|
+
tags: ["engagement", "poll"],
|
|
18125
|
+
version: "1.0.0",
|
|
18126
|
+
provider: OFFICIAL_PROVIDER,
|
|
18127
|
+
commandType: "poll.create"
|
|
18128
|
+
},
|
|
18129
|
+
{
|
|
18130
|
+
id: "official_discussion",
|
|
18131
|
+
name: "Discussion",
|
|
18132
|
+
description: "Whole-class or group real-time discussion thread.",
|
|
18133
|
+
icon: "MessagesSquare",
|
|
18134
|
+
category: "collaboration",
|
|
18135
|
+
permissions: ["lesson:control"],
|
|
18136
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18137
|
+
supportedDevices: ["desktop", "tablet", "all"],
|
|
18138
|
+
tags: ["collaboration", "discussion"],
|
|
18139
|
+
version: "1.0.0",
|
|
18140
|
+
provider: OFFICIAL_PROVIDER,
|
|
18141
|
+
commandType: "discussion.create"
|
|
18142
|
+
},
|
|
18143
|
+
{
|
|
18144
|
+
id: "official_grouping",
|
|
18145
|
+
name: "Grouping",
|
|
18146
|
+
description: "Auto / random / manual student grouping.",
|
|
18147
|
+
icon: "Users",
|
|
18148
|
+
category: "collaboration",
|
|
18149
|
+
permissions: ["lesson:control"],
|
|
18150
|
+
supportedRoles: ["teacher"],
|
|
18151
|
+
supportedDevices: ["desktop", "all"],
|
|
18152
|
+
tags: ["collaboration", "group"],
|
|
18153
|
+
version: "1.0.0",
|
|
18154
|
+
provider: OFFICIAL_PROVIDER,
|
|
18155
|
+
commandType: "grouping.create"
|
|
18156
|
+
},
|
|
18157
|
+
{
|
|
18158
|
+
id: "official_assignment",
|
|
18159
|
+
name: "Assignment",
|
|
18160
|
+
description: "Create, submit and grade student assignments / homework.",
|
|
18161
|
+
icon: "FileText",
|
|
18162
|
+
category: "management",
|
|
18163
|
+
permissions: ["management:write"],
|
|
18164
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18165
|
+
supportedDevices: ["desktop", "tablet", "all"],
|
|
18166
|
+
tags: ["management", "assignment", "homework"],
|
|
18167
|
+
version: "1.0.0",
|
|
18168
|
+
provider: OFFICIAL_PROVIDER,
|
|
18169
|
+
commandType: "assignment.create"
|
|
18170
|
+
},
|
|
18171
|
+
{
|
|
18172
|
+
id: "official_competition",
|
|
18173
|
+
name: "Competition",
|
|
18174
|
+
description: "Timed leaderboard competition between students / groups.",
|
|
18175
|
+
icon: "Trophy",
|
|
18176
|
+
category: "engagement",
|
|
18177
|
+
permissions: ["lesson:control"],
|
|
18178
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18179
|
+
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
18180
|
+
tags: ["engagement", "competition", "gamification"],
|
|
18181
|
+
version: "1.0.0",
|
|
18182
|
+
provider: OFFICIAL_PROVIDER,
|
|
18183
|
+
commandType: "competition.create"
|
|
18184
|
+
},
|
|
18185
|
+
{
|
|
18186
|
+
id: "official_checkin",
|
|
18187
|
+
name: "Check-in",
|
|
18188
|
+
description: "Take class attendance / check-in.",
|
|
18189
|
+
icon: "CheckCircle2",
|
|
18190
|
+
category: "management",
|
|
18191
|
+
permissions: ["management:write"],
|
|
18192
|
+
supportedRoles: ["teacher"],
|
|
18193
|
+
supportedDevices: ["desktop", "tablet", "mobile", "all"],
|
|
18194
|
+
tags: ["management", "attendance", "checkin"],
|
|
18195
|
+
version: "1.0.0",
|
|
18196
|
+
provider: OFFICIAL_PROVIDER,
|
|
18197
|
+
commandType: "attendance.record"
|
|
18198
|
+
},
|
|
18199
|
+
{
|
|
18200
|
+
id: "official_homework",
|
|
18201
|
+
name: "Homework",
|
|
18202
|
+
description: "Assign and track out-of-class homework.",
|
|
18203
|
+
icon: "BookOpen",
|
|
18204
|
+
category: "management",
|
|
18205
|
+
permissions: ["management:write"],
|
|
18206
|
+
supportedRoles: ["teacher", "student", "all"],
|
|
18207
|
+
supportedDevices: ["desktop", "tablet", "all"],
|
|
18208
|
+
tags: ["management", "homework", "assignment"],
|
|
18209
|
+
version: "1.0.0",
|
|
18210
|
+
provider: OFFICIAL_PROVIDER,
|
|
18211
|
+
commandType: "assignment.create"
|
|
18212
|
+
}
|
|
18213
|
+
];
|
|
18214
|
+
function registerOfficialActivities(registry, actionRegistry) {
|
|
18215
|
+
for (const def of OFFICIAL_ACTIVITY_DEFINITIONS) {
|
|
18216
|
+
const descriptor = {
|
|
18217
|
+
...def,
|
|
18218
|
+
aiAction: aiActionFor(def)
|
|
18219
|
+
};
|
|
18220
|
+
registry.registerProvider(new BaseActivityProvider({ descriptor }));
|
|
18221
|
+
if (actionRegistry && descriptor.aiAction) {
|
|
18222
|
+
actionRegistry.register(descriptor.aiAction);
|
|
18223
|
+
}
|
|
18224
|
+
}
|
|
18225
|
+
}
|
|
18226
|
+
|
|
18227
|
+
// packages/activity-ecosystem/index.ts
|
|
18228
|
+
var IActivityRegistryToken = new Token(
|
|
18229
|
+
"@openlearn/activity-ecosystem:IActivityRegistry"
|
|
18230
|
+
);
|
|
18231
|
+
|
|
18232
|
+
// server/routes/os.ts
|
|
18233
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
18234
|
+
var import_fs8 = __toESM(require("fs"), 1);
|
|
18235
|
+
var import_child_process = require("child_process");
|
|
18236
|
+
var import_crypto7 = __toESM(require("crypto"), 1);
|
|
18237
|
+
var import_xss = require("xss");
|
|
18238
|
+
|
|
18239
|
+
// server/utils/crypto.ts
|
|
18240
|
+
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
18241
|
+
var import_fs7 = __toESM(require("fs"), 1);
|
|
18242
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
18243
|
+
var _encryptionKey = null;
|
|
18244
|
+
function getEncryptionKey() {
|
|
18245
|
+
if (_encryptionKey) return _encryptionKey;
|
|
18246
|
+
const envPath = import_path6.default.resolve(process.cwd(), ".env");
|
|
18247
|
+
const keyHex = process.env.ENCRYPTION_KEY;
|
|
18248
|
+
if (keyHex && keyHex.trim() !== "") {
|
|
18249
|
+
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
18250
|
+
return _encryptionKey;
|
|
18251
|
+
}
|
|
18252
|
+
if (import_fs7.default.existsSync(envPath)) {
|
|
18253
|
+
try {
|
|
18254
|
+
const content = import_fs7.default.readFileSync(envPath, "utf-8");
|
|
18255
|
+
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
18256
|
+
if (match && match[1].trim() !== "") {
|
|
18257
|
+
const fileKeyHex = match[1].trim();
|
|
18258
|
+
_encryptionKey = Buffer.from(fileKeyHex, "hex");
|
|
18259
|
+
process.env.ENCRYPTION_KEY = fileKeyHex;
|
|
18260
|
+
console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
|
|
18261
|
+
return _encryptionKey;
|
|
18262
|
+
}
|
|
18263
|
+
} catch {
|
|
18264
|
+
}
|
|
18265
|
+
}
|
|
18266
|
+
const newKey = import_crypto5.default.randomBytes(32).toString("hex");
|
|
18267
|
+
try {
|
|
18268
|
+
if (import_fs7.default.existsSync(envPath)) {
|
|
18269
|
+
let content = import_fs7.default.readFileSync(envPath, "utf-8");
|
|
18270
|
+
if (/^ENCRYPTION_KEY=/m.test(content)) {
|
|
18271
|
+
content = content.replace(/^ENCRYPTION_KEY=.*$/m, `ENCRYPTION_KEY=${newKey}`);
|
|
18272
|
+
import_fs7.default.writeFileSync(envPath, content);
|
|
18273
|
+
console.log("[Crypto] ENCRYPTION_KEY replaced in .env (was empty or invalid)");
|
|
18274
|
+
} else {
|
|
18275
|
+
import_fs7.default.appendFileSync(envPath, `
|
|
18276
|
+
ENCRYPTION_KEY=${newKey}
|
|
18277
|
+
`);
|
|
18278
|
+
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
18279
|
+
}
|
|
18280
|
+
} else {
|
|
18281
|
+
import_fs7.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
18282
|
+
`);
|
|
18283
|
+
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
18284
|
+
}
|
|
18285
|
+
} catch (e) {
|
|
18286
|
+
console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
|
|
18287
|
+
}
|
|
18288
|
+
process.env.ENCRYPTION_KEY = newKey;
|
|
18289
|
+
_encryptionKey = Buffer.from(newKey, "hex");
|
|
18290
|
+
return _encryptionKey;
|
|
18291
|
+
}
|
|
18292
|
+
function encryptApiKey(plaintext) {
|
|
18293
|
+
if (!plaintext) return "";
|
|
18294
|
+
const key = getEncryptionKey();
|
|
18295
|
+
const iv = import_crypto5.default.randomBytes(16);
|
|
18296
|
+
const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
|
|
18297
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
18298
|
+
const authTag = cipher.getAuthTag();
|
|
18299
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
18300
|
+
}
|
|
18301
|
+
function decryptApiKey(encrypted) {
|
|
18302
|
+
if (!encrypted) return "";
|
|
18303
|
+
const parts = encrypted.split(":");
|
|
18304
|
+
if (parts.length !== 3) return encrypted;
|
|
18305
|
+
try {
|
|
18306
|
+
const key = getEncryptionKey();
|
|
18307
|
+
const iv = Buffer.from(parts[0], "hex");
|
|
18308
|
+
const authTag = Buffer.from(parts[1], "hex");
|
|
18309
|
+
const ciphertext = Buffer.from(parts[2], "hex");
|
|
18310
|
+
const decipher = import_crypto5.default.createDecipheriv("aes-256-gcm", key, iv);
|
|
18311
|
+
decipher.setAuthTag(authTag);
|
|
18312
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
18313
|
+
} catch {
|
|
18314
|
+
console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
|
|
18315
|
+
return encrypted;
|
|
18316
|
+
}
|
|
18317
|
+
}
|
|
18318
|
+
function maskApiKey(key) {
|
|
18319
|
+
if (!key || key.length <= 8) return key ? "****" : "";
|
|
18320
|
+
return key.substring(0, 4) + "****" + key.substring(key.length - 4);
|
|
18321
|
+
}
|
|
18322
|
+
var PROMPT_INJECTION_PATTERNS = [
|
|
18323
|
+
/ignore\s+(all\s+)?(previous|prior|above|system)\s+(instructions?|prompts?|directives?)/i,
|
|
18324
|
+
/you\s+are\s+now\s+(a\s+)?(different|new|another)/i,
|
|
18325
|
+
/forget\s+(all\s+)?(your|the)\s+(training|instructions?|rules?)/i,
|
|
18326
|
+
/system\s*(prompt|message|instruction):\s*/i,
|
|
18327
|
+
/<\|im_start\|>/i,
|
|
18328
|
+
/<\|im_end\|>/i
|
|
18329
|
+
];
|
|
18330
|
+
function detectPromptInjection(input) {
|
|
18331
|
+
return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
|
|
18332
|
+
}
|
|
18333
|
+
|
|
18334
|
+
// server/middleware/auth.ts
|
|
18335
|
+
function getValidSession(token) {
|
|
18336
|
+
let sessionRow;
|
|
18337
|
+
try {
|
|
18338
|
+
sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
18339
|
+
} catch {
|
|
18340
|
+
return null;
|
|
18341
|
+
}
|
|
18342
|
+
if (!sessionRow) return null;
|
|
18343
|
+
const now = Date.now();
|
|
18344
|
+
if (sessionRow.expires_at && sessionRow.expires_at < now) {
|
|
18345
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
18346
|
+
return null;
|
|
18347
|
+
}
|
|
18348
|
+
const idleTimeout = 24 * 60 * 60 * 1e3;
|
|
18349
|
+
if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
|
|
18350
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
18351
|
+
return null;
|
|
18352
|
+
}
|
|
18353
|
+
kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
|
|
18354
|
+
return JSON.parse(sessionRow.session_data);
|
|
18355
|
+
}
|
|
18356
|
+
function getCookieToken(req) {
|
|
18357
|
+
const rc = req.headers.cookie;
|
|
18358
|
+
if (!rc) return null;
|
|
18359
|
+
const parts = rc.split(";");
|
|
18360
|
+
for (const part of parts) {
|
|
18361
|
+
const trimmed = part.trim();
|
|
18362
|
+
if (trimmed.startsWith("edu_os_token=")) {
|
|
18363
|
+
return trimmed.substring("edu_os_token=".length);
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
return null;
|
|
18367
|
+
}
|
|
18368
|
+
function getActorId(req) {
|
|
18369
|
+
const token = getCookieToken(req);
|
|
18370
|
+
if (!token) return "user-frontend";
|
|
18371
|
+
try {
|
|
18372
|
+
const session = getValidSession(token);
|
|
18373
|
+
if (!session) return "user-frontend";
|
|
18374
|
+
let role = session.subRole || session.role;
|
|
18375
|
+
if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
|
|
18376
|
+
role = "administrator";
|
|
18377
|
+
}
|
|
18378
|
+
if (role) {
|
|
18379
|
+
return `user:${session.userId || "demo"}:${role}`;
|
|
18380
|
+
}
|
|
18381
|
+
return "user-frontend";
|
|
18382
|
+
} catch {
|
|
18383
|
+
return "user-frontend";
|
|
18384
|
+
}
|
|
18385
|
+
}
|
|
18386
|
+
function checkIsTeacherOrAdmin(req) {
|
|
18387
|
+
const token = getCookieToken(req);
|
|
18388
|
+
if (!token) return false;
|
|
18389
|
+
try {
|
|
18390
|
+
const session = getValidSession(token);
|
|
18391
|
+
if (!session) return false;
|
|
18392
|
+
return session.role === "teacher" || session.role === "administrator";
|
|
18393
|
+
} catch {
|
|
18394
|
+
return false;
|
|
18395
|
+
}
|
|
18396
|
+
}
|
|
18397
|
+
|
|
18398
|
+
// server/routes/shared.ts
|
|
18399
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
18400
|
+
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
18401
|
+
|
|
18402
|
+
// server/utils/bridge-sdk.ts
|
|
18403
|
+
var BRIDGE_SDK_CODE = `(function() {
|
|
18404
|
+
// Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
|
|
18405
|
+
try {
|
|
18406
|
+
Object.defineProperty(document, 'cookie', {
|
|
18407
|
+
get: function() { return ""; },
|
|
18408
|
+
set: function(val) {},
|
|
18409
|
+
configurable: true
|
|
18410
|
+
});
|
|
18411
|
+
} catch (e) {
|
|
18412
|
+
try {
|
|
18413
|
+
Object.defineProperty(Document.prototype, 'cookie', {
|
|
18414
|
+
get: function() { return ""; },
|
|
18415
|
+
set: function(val) {},
|
|
18416
|
+
configurable: true
|
|
18417
|
+
});
|
|
18418
|
+
} catch (err) {}
|
|
18419
|
+
}
|
|
18420
|
+
|
|
18421
|
+
// \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
|
|
18422
|
+
function __makeSafePostMessage(realTarget, label) {
|
|
18423
|
+
return function(message, targetOrigin, transfer) {
|
|
18424
|
+
try {
|
|
18425
|
+
if (message && typeof message === 'object') {
|
|
18426
|
+
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18427
|
+
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18428
|
+
}
|
|
18429
|
+
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18430
|
+
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18431
|
+
}
|
|
18432
|
+
}
|
|
18433
|
+
} catch (e) {}
|
|
18434
|
+
|
|
18435
|
+
var origin = targetOrigin;
|
|
18436
|
+
if (origin === 'null' || origin === null || origin === undefined) {
|
|
18437
|
+
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
|
|
18438
|
+
origin = '*';
|
|
18439
|
+
}
|
|
18440
|
+
try {
|
|
18441
|
+
return realTarget.postMessage.call(realTarget, message, origin, transfer);
|
|
18442
|
+
} catch (err) {
|
|
18443
|
+
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18444
|
+
console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
|
|
18445
|
+
return realTarget.postMessage.call(realTarget, message, '*', transfer);
|
|
18446
|
+
}
|
|
18447
|
+
throw err;
|
|
18448
|
+
}
|
|
18449
|
+
};
|
|
18450
|
+
}
|
|
18451
|
+
|
|
18452
|
+
// \u2500\u2500 Helper: create a Proxy wrapper around a cross-origin WindowProxy \u2500\u2500
|
|
18453
|
+
// This is necessary because sandboxed iframes (without allow-same-origin)
|
|
18454
|
+
// cannot set properties on cross-origin WindowProxy objects.
|
|
18455
|
+
// We use Object.defineProperty to shadow window.parent / window.top
|
|
18456
|
+
// with a Proxy that intercepts the .postMessage() call.
|
|
18457
|
+
function __proxyWindow(realRef, propName) {
|
|
18458
|
+
try {
|
|
18459
|
+
var safePost = __makeSafePostMessage(realRef, propName + '.postMessage');
|
|
18460
|
+
var proxyObj = new Proxy(realRef, {
|
|
18461
|
+
get: function(target, prop) {
|
|
18462
|
+
if (prop === 'postMessage') return safePost;
|
|
18463
|
+
try {
|
|
18464
|
+
var val = target[prop];
|
|
18465
|
+
if (typeof val === 'function') return val.bind(target);
|
|
18466
|
+
return val;
|
|
18467
|
+
} catch (e) { return undefined; }
|
|
18468
|
+
}
|
|
18469
|
+
});
|
|
18470
|
+
Object.defineProperty(window, propName, {
|
|
18471
|
+
get: function() { return proxyObj; },
|
|
18472
|
+
configurable: true
|
|
18473
|
+
});
|
|
18474
|
+
} catch (e) {
|
|
18475
|
+
// Proxy or defineProperty not supported, fall back to direct override attempt
|
|
18476
|
+
try { realRef.postMessage = __makeSafePostMessage(realRef, propName + '.postMessage (fallback)'); } catch (_) {}
|
|
18477
|
+
}
|
|
18478
|
+
}
|
|
18479
|
+
|
|
18480
|
+
// Proxy postMessage calls to enrich them with attempt_id/uuid and normalize targetOrigin
|
|
18481
|
+
try {
|
|
18482
|
+
// 1. Override window.postMessage (self-targeting, always works)
|
|
18483
|
+
var originalPostMessage = window.postMessage;
|
|
18484
|
+
window.postMessage = function(message, targetOrigin, transfer) {
|
|
18485
|
+
try {
|
|
18486
|
+
if (message && typeof message === 'object') {
|
|
18487
|
+
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18488
|
+
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18489
|
+
}
|
|
18490
|
+
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18491
|
+
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18492
|
+
}
|
|
18493
|
+
}
|
|
18494
|
+
} catch (e) {}
|
|
18495
|
+
|
|
18496
|
+
var origin = targetOrigin;
|
|
18497
|
+
if (origin === 'null' || origin === null || origin === undefined) {
|
|
18498
|
+
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "null" to "*" for postMessage call');
|
|
18499
|
+
origin = '*';
|
|
18500
|
+
}
|
|
18501
|
+
try {
|
|
18502
|
+
return originalPostMessage.call(this, message, origin, transfer);
|
|
18503
|
+
} catch (err) {
|
|
18504
|
+
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18505
|
+
console.warn('[LMS Bridge Notice] Recovered SyntaxError on postMessage, fallback targetOrigin to "*"', err);
|
|
18506
|
+
return originalPostMessage.call(this, message, '*', transfer);
|
|
18507
|
+
}
|
|
18508
|
+
throw err;
|
|
18509
|
+
}
|
|
18510
|
+
};
|
|
18511
|
+
|
|
18512
|
+
// 2. Shadow window.parent with a Proxy that intercepts .postMessage()
|
|
18513
|
+
if (window.parent && window.parent !== window) {
|
|
18514
|
+
__proxyWindow(window.parent, 'parent');
|
|
18515
|
+
}
|
|
18516
|
+
|
|
18517
|
+
// 3. Shadow window.top with a Proxy that intercepts .postMessage()
|
|
18518
|
+
try {
|
|
18519
|
+
if (window.top && window.top !== window) {
|
|
18520
|
+
__proxyWindow(window.top, 'top');
|
|
18521
|
+
}
|
|
18522
|
+
} catch (e) {}
|
|
18523
|
+
|
|
18524
|
+
// 4. Intercept message event listeners to sanitize event.source.postMessage replies
|
|
18525
|
+
var origAddEventListener = window.addEventListener;
|
|
18526
|
+
if (typeof origAddEventListener === 'function') {
|
|
18527
|
+
window.addEventListener = function(type, listener, options) {
|
|
18528
|
+
if (type === 'message' && typeof listener === 'function') {
|
|
18529
|
+
var wrappedListener = function(event) {
|
|
18530
|
+
try {
|
|
18531
|
+
if (event && event.source && typeof event.source.postMessage === 'function') {
|
|
18532
|
+
var origSourcePostMessage = event.source.postMessage;
|
|
18533
|
+
event.source.postMessage = function(msg, targetOrigin, transfer) {
|
|
18534
|
+
var origin = targetOrigin;
|
|
18535
|
+
if (origin === 'null' || origin === null) {
|
|
18536
|
+
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "null" to "*" on event.source.postMessage');
|
|
18537
|
+
origin = '*';
|
|
18538
|
+
}
|
|
18539
|
+
try {
|
|
18540
|
+
return origSourcePostMessage.call(event.source, msg, origin, transfer);
|
|
18541
|
+
} catch (err) {
|
|
18542
|
+
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18543
|
+
console.warn('[LMS Bridge Notice] Recovered SyntaxError on event.source.postMessage, fallback to "*"', err);
|
|
18544
|
+
return origSourcePostMessage.call(event.source, msg, '*', transfer);
|
|
18545
|
+
}
|
|
18546
|
+
throw err;
|
|
18547
|
+
}
|
|
18548
|
+
};
|
|
18549
|
+
}
|
|
18550
|
+
} catch (e) {}
|
|
18551
|
+
return listener.apply(this, arguments);
|
|
18552
|
+
};
|
|
18553
|
+
return origAddEventListener.call(this, type, wrappedListener, options);
|
|
18554
|
+
}
|
|
18555
|
+
return origAddEventListener.apply(this, arguments);
|
|
18556
|
+
};
|
|
18557
|
+
}
|
|
18558
|
+
} catch (e) {}
|
|
18559
|
+
|
|
18560
|
+
window.LMS = {
|
|
18561
|
+
submit(data) {
|
|
18562
|
+
window.parent.postMessage({
|
|
18563
|
+
type: "LMS_SUBMIT",
|
|
18564
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18565
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18566
|
+
payload: data
|
|
18567
|
+
}, "*");
|
|
18568
|
+
},
|
|
18569
|
+
saveProgress(data) {
|
|
18570
|
+
window.parent.postMessage({
|
|
18571
|
+
type: "LMS_SAVE_PROGRESS",
|
|
18572
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18573
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18574
|
+
payload: data
|
|
18575
|
+
}, "*");
|
|
18576
|
+
},
|
|
18577
|
+
finish(data) {
|
|
18578
|
+
window.parent.postMessage({
|
|
18579
|
+
type: "LMS_FINISH",
|
|
18580
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18581
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18582
|
+
payload: data
|
|
18583
|
+
}, "*");
|
|
18584
|
+
},
|
|
18585
|
+
getStudent() {
|
|
18586
|
+
return window.__LMS_STUDENT__;
|
|
18587
|
+
},
|
|
18588
|
+
getCourseware() {
|
|
18589
|
+
return window.__LMS_COURSEWARE__;
|
|
18590
|
+
},
|
|
18591
|
+
log(event, data) {
|
|
18592
|
+
window.parent.postMessage({
|
|
18593
|
+
type: "LMS_LOG",
|
|
18594
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18595
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18596
|
+
event: event,
|
|
18597
|
+
payload: data
|
|
18598
|
+
}, "*");
|
|
18599
|
+
}
|
|
18600
|
+
};
|
|
18601
|
+
|
|
18602
|
+
try {
|
|
18603
|
+
if (window.fetch) {
|
|
18604
|
+
const originalFetch = window.fetch;
|
|
18605
|
+
window.fetch = function(input, init) {
|
|
18606
|
+
try {
|
|
18607
|
+
const url = (typeof input === 'string') ? input : (input?.url || '');
|
|
18608
|
+
const method = init?.method || input?.method || 'GET';
|
|
18609
|
+
const headers = init?.headers || input?.headers || {};
|
|
18610
|
+
let body = init?.body || input?.body || null;
|
|
18611
|
+
|
|
18612
|
+
if (body && typeof body === 'object') {
|
|
18613
|
+
try { body = JSON.stringify(body); } catch(e){}
|
|
18614
|
+
}
|
|
18615
|
+
|
|
18616
|
+
if (url && !url.includes('/api/courseware/attempts/')) {
|
|
18617
|
+
window.parent.postMessage({
|
|
18618
|
+
type: "HOOK_FETCH",
|
|
18619
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18620
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18621
|
+
payload: { url, method, headers: JSON.parse(JSON.stringify(headers)), body: body ? body.toString() : null }
|
|
18622
|
+
}, "*");
|
|
18623
|
+
}
|
|
18624
|
+
} catch (e) {
|
|
18625
|
+
console.error("Bridge Hook fetch error", e);
|
|
18626
|
+
}
|
|
18627
|
+
return originalFetch.apply(this, arguments);
|
|
18628
|
+
};
|
|
18629
|
+
}
|
|
18630
|
+
|
|
18631
|
+
if (window.XMLHttpRequest) {
|
|
18632
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
18633
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
18634
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
18635
|
+
this._method = method;
|
|
18636
|
+
this._url = url;
|
|
18637
|
+
return originalOpen.apply(this, arguments);
|
|
18638
|
+
};
|
|
18639
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
18640
|
+
try {
|
|
18641
|
+
let bodyStr = body;
|
|
18642
|
+
if (body && typeof body === 'object') {
|
|
18643
|
+
try { bodyStr = JSON.stringify(body); } catch(e){}
|
|
18644
|
+
}
|
|
18645
|
+
if (this._url && !this._url.includes('/api/courseware/attempts/')) {
|
|
18646
|
+
window.parent.postMessage({
|
|
18647
|
+
type: "HOOK_XHR",
|
|
18648
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18649
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18650
|
+
payload: { url: this._url, method: this._method, body: bodyStr ? bodyStr.toString() : null }
|
|
18651
|
+
}, "*");
|
|
18652
|
+
}
|
|
18653
|
+
} catch (e) {
|
|
18654
|
+
console.error("Bridge Hook XHR error", e);
|
|
18655
|
+
}
|
|
18656
|
+
return originalSend.apply(this, arguments);
|
|
18657
|
+
};
|
|
18658
|
+
}
|
|
18659
|
+
|
|
18660
|
+
function attachToAxios(axiosInstance) {
|
|
18661
|
+
if (axiosInstance && axiosInstance.interceptors && axiosInstance.interceptors.request) {
|
|
18662
|
+
axiosInstance.interceptors.request.use(function(config) {
|
|
18663
|
+
try {
|
|
18664
|
+
if (config.url && !config.url.includes('/api/courseware/attempts/')) {
|
|
18665
|
+
window.parent.postMessage({
|
|
18666
|
+
type: "HOOK_AXIOS",
|
|
18667
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18668
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18669
|
+
payload: { url: config.url, method: config.method, data: config.data }
|
|
18670
|
+
}, "*");
|
|
18671
|
+
}
|
|
18672
|
+
} catch (e) {
|
|
18673
|
+
console.error("Bridge Hook Axios error", e);
|
|
18674
|
+
}
|
|
18675
|
+
return config;
|
|
18676
|
+
}, function(error) { return Promise.reject(error); });
|
|
18677
|
+
}
|
|
18678
|
+
}
|
|
18679
|
+
if (window.axios) {
|
|
18680
|
+
attachToAxios(window.axios);
|
|
18681
|
+
}
|
|
18682
|
+
var _axios = window.axios;
|
|
18683
|
+
Object.defineProperty(window, 'axios', {
|
|
18684
|
+
get: function() { return _axios; },
|
|
18685
|
+
set: function(val) {
|
|
18686
|
+
_axios = val;
|
|
18687
|
+
attachToAxios(val);
|
|
18688
|
+
},
|
|
18689
|
+
configurable: true
|
|
18690
|
+
});
|
|
18691
|
+
|
|
18692
|
+
if (navigator && navigator.sendBeacon) {
|
|
18693
|
+
const originalSendBeacon = navigator.sendBeacon;
|
|
18694
|
+
navigator.sendBeacon = function(url, data) {
|
|
18695
|
+
try {
|
|
18696
|
+
if (url && !url.includes('/api/courseware/attempts/')) {
|
|
18697
|
+
window.parent.postMessage({
|
|
18698
|
+
type: "HOOK_BEACON",
|
|
18699
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18700
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18701
|
+
payload: { url: url, data: data ? data.toString() : null }
|
|
18702
|
+
}, "*");
|
|
18703
|
+
}
|
|
18704
|
+
} catch (e) {
|
|
18705
|
+
console.error("Bridge Hook Beacon error", e);
|
|
18706
|
+
}
|
|
18707
|
+
return originalSendBeacon.apply(this, arguments);
|
|
18708
|
+
};
|
|
18709
|
+
}
|
|
18710
|
+
|
|
18711
|
+
window.addEventListener('submit', function(e) {
|
|
18712
|
+
try {
|
|
18713
|
+
const form = e.target;
|
|
18714
|
+
const formData = new FormData(form);
|
|
18715
|
+
const data = {};
|
|
18716
|
+
formData.forEach(function(value, key) {
|
|
18717
|
+
data[key] = value;
|
|
18718
|
+
});
|
|
18719
|
+
if (form.action && !form.action.includes('/api/courseware/attempts/')) {
|
|
18720
|
+
window.parent.postMessage({
|
|
18721
|
+
type: "HOOK_FORM",
|
|
18722
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
18723
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
18724
|
+
payload: { action: form.action, method: form.method, data: data }
|
|
18725
|
+
}, "*");
|
|
18726
|
+
}
|
|
18727
|
+
} catch (err) {
|
|
18728
|
+
console.error("Bridge Hook Form error", err);
|
|
18729
|
+
}
|
|
18730
|
+
}, true);
|
|
18731
|
+
|
|
18732
|
+
// --- SMART DOM SCRAPER FOR GENERIC COURSEWARES ---
|
|
18733
|
+
function logToServer(msg, detail) {
|
|
18734
|
+
// \u4EC5 console \u8F93\u51FA\uFF1B\u7F51\u7EDC\u8BF7\u6C42\u53EF\u80FD\u88AB\u6D4F\u89C8\u5668 HTTPS \u5347\u7EA7\u5BFC\u81F4 ERR_CONNECTION_REFUSED
|
|
18735
|
+
try {
|
|
18736
|
+
console.log('[LMS Debug]', msg, detail || '');
|
|
18737
|
+
} catch (e) {}
|
|
18738
|
+
}
|
|
18739
|
+
|
|
18740
|
+
function findScoreInDOM() {
|
|
18741
|
+
const logData = [];
|
|
18742
|
+
try {
|
|
18743
|
+
const commonVars = ['score', 'points', 'grade', 'totalScore', 'currentScore', 'userScore', 'finalScore', 'correctCount'];
|
|
18744
|
+
for (const v of commonVars) {
|
|
18745
|
+
if (typeof window[v] === 'number') {
|
|
18746
|
+
logData.push("Global var " + v + " is number: " + window[v]);
|
|
18747
|
+
return { score: window[v], log: logData };
|
|
18748
|
+
}
|
|
18749
|
+
if (typeof window[v] === 'string') {
|
|
18750
|
+
const num = parseFloat(window[v]);
|
|
18751
|
+
if (!isNaN(num)) {
|
|
18752
|
+
logData.push("Global var " + v + " is string with number: " + window[v]);
|
|
18753
|
+
return { score: num, log: logData };
|
|
18754
|
+
}
|
|
18755
|
+
}
|
|
18756
|
+
}
|
|
18757
|
+
|
|
18758
|
+
const selectors = [
|
|
18759
|
+
'#score', '#scoreDisplay', '#score-num', '#scoreDisplaySpan', '#points', '#grade',
|
|
18760
|
+
'.score', '.points', '.grade', '.score-num', '.score-value',
|
|
18761
|
+
'[id*="score" i]', '[id*="point" i]', '[id*="grade" i]', '[id*="result" i]',
|
|
18762
|
+
'[class*="score" i]', '[class*="point" i]', '[class*="grade" i]', '[class*="result" i]'
|
|
18763
|
+
];
|
|
18764
|
+
|
|
18765
|
+
for (const selector of selectors) {
|
|
18766
|
+
try {
|
|
18767
|
+
const el = document.querySelector(selector);
|
|
18768
|
+
if (el) {
|
|
18769
|
+
const text = (el.textContent || el.innerText || '').trim();
|
|
18770
|
+
if (text) {
|
|
18771
|
+
logData.push("Selector '" + selector + "' matched text: '" + text + "'");
|
|
18772
|
+
const fractionMatch = text.match(/(\\\\d+(\\\\.\\\\d+)?)\\\\s*[\\\\/|\u4E4B]\\\\s*(\\\\d+)/);
|
|
18773
|
+
if (fractionMatch) {
|
|
18774
|
+
const num = parseFloat(fractionMatch[1]);
|
|
18775
|
+
const den = parseFloat(fractionMatch[3]);
|
|
18776
|
+
if (den > 0) {
|
|
18777
|
+
const pct = (num / den) * 100;
|
|
18778
|
+
logData.push("Parsed fraction: " + num + "/" + den + " -> " + pct);
|
|
18779
|
+
return { score: pct, log: logData };
|
|
18780
|
+
}
|
|
18781
|
+
}
|
|
18782
|
+
const match = text.match(/\\\\d+(\\\\.\\\\d+)?/);
|
|
18783
|
+
if (match) {
|
|
18784
|
+
const num = parseFloat(match[0]);
|
|
18785
|
+
if (!isNaN(num)) {
|
|
18786
|
+
logData.push("Parsed decimal: " + num);
|
|
18787
|
+
return { score: num, log: logData };
|
|
18788
|
+
}
|
|
18789
|
+
}
|
|
18790
|
+
}
|
|
18791
|
+
}
|
|
18792
|
+
} catch (e) {}
|
|
18793
|
+
}
|
|
18794
|
+
|
|
18795
|
+
try {
|
|
18796
|
+
const inputs = document.querySelectorAll('input[type="text"], input[type="number"], input[readonly]');
|
|
18797
|
+
for (const input of inputs) {
|
|
18798
|
+
const id = (input.id || '').toLowerCase();
|
|
18799
|
+
const name = (input.name || '').toLowerCase();
|
|
18800
|
+
if (id.includes('score') || name.includes('score') || id.includes('point') || name.includes('point')) {
|
|
18801
|
+
const val = parseFloat(input.value);
|
|
18802
|
+
if (!isNaN(val)) {
|
|
18803
|
+
logData.push("Input id=" + id + " name=" + name + " value: " + input.value);
|
|
18804
|
+
return { score: val, log: logData };
|
|
18805
|
+
}
|
|
18806
|
+
}
|
|
18807
|
+
}
|
|
18808
|
+
} catch (e) {}
|
|
18809
|
+
|
|
18810
|
+
try {
|
|
18811
|
+
const all = document.getElementsByTagName('*');
|
|
18089
18812
|
const ignoredTags = ['style', 'script', 'link', 'meta', 'svg', 'canvas', 'noscript', 'head', 'iframe'];
|
|
18090
18813
|
for (let i = 0; i < all.length; i++) {
|
|
18091
18814
|
const el = all[i];
|
|
@@ -18264,7 +18987,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
18264
18987
|
const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
|
|
18265
18988
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, session.studentId, "active");
|
|
18266
18989
|
if (!attempt) {
|
|
18267
|
-
const attemptId = "att_" +
|
|
18990
|
+
const attemptId = "att_" + import_crypto6.default.randomBytes(8).toString("hex");
|
|
18268
18991
|
kernelContainer.db.prepare("INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)").run(attemptId, cwInfo.id, session.studentId, Date.now(), "active");
|
|
18269
18992
|
attempt = { id: attemptId };
|
|
18270
18993
|
}
|
|
@@ -18277,7 +19000,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
18277
19000
|
} else if (session.role === "teacher" || session.role === "administrator") {
|
|
18278
19001
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
|
|
18279
19002
|
if (!attempt) {
|
|
18280
|
-
const attemptId = "att_teacher_" +
|
|
19003
|
+
const attemptId = "att_teacher_" + import_crypto6.default.randomBytes(8).toString("hex");
|
|
18281
19004
|
kernelContainer.db.prepare("INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)").run(attemptId, cwInfo.id, "teacher", Date.now(), "active");
|
|
18282
19005
|
attempt = { id: attemptId };
|
|
18283
19006
|
}
|
|
@@ -18293,7 +19016,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
18293
19016
|
if (studentInfo.attempt_id === "guest-attempt") {
|
|
18294
19017
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
|
|
18295
19018
|
if (!attempt) {
|
|
18296
|
-
const attemptId = "att_guest_" +
|
|
19019
|
+
const attemptId = "att_guest_" + import_crypto6.default.randomBytes(8).toString("hex");
|
|
18297
19020
|
kernelContainer.db.prepare("INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)").run(attemptId, cwInfo.id, "guest", Date.now(), "active");
|
|
18298
19021
|
attempt = { id: attemptId };
|
|
18299
19022
|
}
|
|
@@ -18381,7 +19104,7 @@ function registerOsRoutes(ctx) {
|
|
|
18381
19104
|
if (!import_fs8.default.existsSync(uploadsDir)) {
|
|
18382
19105
|
import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
|
|
18383
19106
|
}
|
|
18384
|
-
const uniqueName = `${Date.now()}-${
|
|
19107
|
+
const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
|
|
18385
19108
|
const filePath = import_path8.default.join(uploadsDir, uniqueName);
|
|
18386
19109
|
import_fs8.default.writeFileSync(filePath, fileBuffer);
|
|
18387
19110
|
let slideCount = 1;
|
|
@@ -18591,8 +19314,8 @@ function registerOsRoutes(ctx) {
|
|
|
18591
19314
|
const result = provider ? await runOpenAIAgentChat2(provider, { message, lang, currentLessonId, attachments, callerRole, history }) : await runGeminiAgentChat2({ message, lang, currentLessonId, attachments, callerRole, history });
|
|
18592
19315
|
if (result && typeof result.agentText === "string") {
|
|
18593
19316
|
const now = Date.now();
|
|
18594
|
-
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" +
|
|
18595
|
-
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" +
|
|
19317
|
+
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto7.default.randomUUID(), convKey, "user", message, now);
|
|
19318
|
+
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto7.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
|
|
18596
19319
|
}
|
|
18597
19320
|
res.json({
|
|
18598
19321
|
success: true,
|
|
@@ -18934,7 +19657,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
18934
19657
|
// server/routes/courseware.ts
|
|
18935
19658
|
var import_path9 = __toESM(require("path"), 1);
|
|
18936
19659
|
var import_fs9 = __toESM(require("fs"), 1);
|
|
18937
|
-
var
|
|
19660
|
+
var import_crypto9 = __toESM(require("crypto"), 1);
|
|
18938
19661
|
function registerCoursewareRoutes(ctx) {
|
|
18939
19662
|
const {
|
|
18940
19663
|
app,
|
|
@@ -19079,7 +19802,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19079
19802
|
try {
|
|
19080
19803
|
const { attemptId } = req.params;
|
|
19081
19804
|
const { eventType, payload } = req.body;
|
|
19082
|
-
const rawId = "raw_" +
|
|
19805
|
+
const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
|
|
19083
19806
|
kernelContainer.db.prepare(
|
|
19084
19807
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
19085
19808
|
).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
|
|
@@ -19107,7 +19830,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19107
19830
|
kernelContainer.db.prepare(
|
|
19108
19831
|
"INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
|
|
19109
19832
|
).run(
|
|
19110
|
-
"res_" +
|
|
19833
|
+
"res_" + import_crypto9.default.randomBytes(8).toString("hex"),
|
|
19111
19834
|
attemptId,
|
|
19112
19835
|
parsedScore,
|
|
19113
19836
|
comment || null,
|
|
@@ -19258,7 +19981,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19258
19981
|
).get(classId, lessonId, assignmentTitle);
|
|
19259
19982
|
let assignmentId = assignment?.id;
|
|
19260
19983
|
if (!assignmentId) {
|
|
19261
|
-
assignmentId = "ast-cw-" +
|
|
19984
|
+
assignmentId = "ast-cw-" + import_crypto9.default.randomBytes(8).toString("hex");
|
|
19262
19985
|
kernelContainer.db.prepare(
|
|
19263
19986
|
"INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
19264
19987
|
).run(
|
|
@@ -19415,7 +20138,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
19415
20138
|
}
|
|
19416
20139
|
|
|
19417
20140
|
// server/routes/lessons.ts
|
|
19418
|
-
var
|
|
20141
|
+
var import_genai3 = require("@google/genai");
|
|
19419
20142
|
function registerLessonsRoutes(ctx) {
|
|
19420
20143
|
const {
|
|
19421
20144
|
app,
|
|
@@ -19796,7 +20519,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
19796
20519
|
app.post("/api/lessons/:id/ai-tutor", async (req, res) => {
|
|
19797
20520
|
try {
|
|
19798
20521
|
const { elements } = req.body;
|
|
19799
|
-
const ai = new
|
|
20522
|
+
const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
19800
20523
|
const elementsSummary = elements.map((e, i) => `Element ${i + 1}: type=${e.type}, content=${JSON.stringify(e.data)}`).join("\n");
|
|
19801
20524
|
const prompt = `You are a real-time AI Tutor monitoring a student's interactive whiteboard.
|
|
19802
20525
|
The student has pressed the "Ask AI" button for help.
|
|
@@ -20226,7 +20949,7 @@ function registerAdminRoutes(ctx) {
|
|
|
20226
20949
|
// server/routes/roster.ts
|
|
20227
20950
|
var import_path12 = __toESM(require("path"), 1);
|
|
20228
20951
|
var import_fs11 = __toESM(require("fs"), 1);
|
|
20229
|
-
var
|
|
20952
|
+
var import_crypto10 = __toESM(require("crypto"), 1);
|
|
20230
20953
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
20231
20954
|
function registerRosterRoutes(ctx) {
|
|
20232
20955
|
const {
|
|
@@ -20486,7 +21209,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20486
21209
|
if (storedPwd.startsWith("$2")) {
|
|
20487
21210
|
matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
|
|
20488
21211
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
20489
|
-
matches =
|
|
21212
|
+
matches = import_crypto10.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
|
|
20490
21213
|
} else {
|
|
20491
21214
|
matches = storedPwd === oldPassword;
|
|
20492
21215
|
}
|
|
@@ -20558,7 +21281,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20558
21281
|
}
|
|
20559
21282
|
const avatarDir = import_path12.default.join(process.cwd(), "uploads", "avatars");
|
|
20560
21283
|
import_fs11.default.mkdirSync(avatarDir, { recursive: true });
|
|
20561
|
-
const uniqueName = `${Date.now()}-${
|
|
21284
|
+
const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
|
|
20562
21285
|
const filePath = import_path12.default.join(avatarDir, uniqueName);
|
|
20563
21286
|
import_fs11.default.writeFileSync(filePath, fileBuffer);
|
|
20564
21287
|
const avatarUrl = `/uploads/avatars/${uniqueName}`;
|
|
@@ -20677,7 +21400,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20677
21400
|
if (storedPwd.startsWith("$2")) {
|
|
20678
21401
|
matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
|
|
20679
21402
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
20680
|
-
const sha256Hash =
|
|
21403
|
+
const sha256Hash = import_crypto10.default.createHash("sha256").update(providedPassword).digest("hex");
|
|
20681
21404
|
if (sha256Hash === storedPwd) {
|
|
20682
21405
|
matchesOwnPassword = true;
|
|
20683
21406
|
kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
|
|
@@ -20716,7 +21439,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20716
21439
|
};
|
|
20717
21440
|
}
|
|
20718
21441
|
if (sessionData) {
|
|
20719
|
-
const sessionToken = "token_" +
|
|
21442
|
+
const sessionToken = "token_" + import_crypto10.default.randomBytes(16).toString("hex");
|
|
20720
21443
|
const now = Date.now();
|
|
20721
21444
|
const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
|
|
20722
21445
|
kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
|
|
@@ -21295,7 +22018,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21295
22018
|
}
|
|
21296
22019
|
|
|
21297
22020
|
// server/routes/assignments.ts
|
|
21298
|
-
var
|
|
22021
|
+
var import_genai4 = require("@google/genai");
|
|
21299
22022
|
function registerAssignmentsRoutes(ctx) {
|
|
21300
22023
|
const {
|
|
21301
22024
|
app,
|
|
@@ -21323,7 +22046,7 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
21323
22046
|
app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
|
|
21324
22047
|
try {
|
|
21325
22048
|
const { topic, lessonId } = req.body;
|
|
21326
|
-
const ai = new
|
|
22049
|
+
const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
21327
22050
|
const prompt = `You are an expert teacher. Generate a short 1-question quiz or assignment about "${topic}". Output in this JSON format: {"title": "...", "description": "...", "content": "..."} without markdown blocks.`;
|
|
21328
22051
|
const response = await ai.models.generateContent({ model: "gemini-3.5-flash", contents: prompt });
|
|
21329
22052
|
const text = response.text || "{}";
|
|
@@ -21355,7 +22078,7 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
21355
22078
|
if (!lesson) {
|
|
21356
22079
|
return res.status(404).json({ error: "Lesson not found" });
|
|
21357
22080
|
}
|
|
21358
|
-
const ai = new
|
|
22081
|
+
const ai = new import_genai4.GoogleGenAI({
|
|
21359
22082
|
apiKey: process.env.GEMINI_API_KEY,
|
|
21360
22083
|
httpOptions: {
|
|
21361
22084
|
headers: {
|
|
@@ -21380,26 +22103,26 @@ Generate the response in the specified JSON schema.`;
|
|
|
21380
22103
|
config: {
|
|
21381
22104
|
responseMimeType: "application/json",
|
|
21382
22105
|
responseSchema: {
|
|
21383
|
-
type:
|
|
22106
|
+
type: import_genai4.Type.OBJECT,
|
|
21384
22107
|
properties: {
|
|
21385
22108
|
learningObjectives: {
|
|
21386
|
-
type:
|
|
21387
|
-
items: { type:
|
|
22109
|
+
type: import_genai4.Type.ARRAY,
|
|
22110
|
+
items: { type: import_genai4.Type.STRING },
|
|
21388
22111
|
description: "List of identified key learning objectives for the lesson"
|
|
21389
22112
|
},
|
|
21390
22113
|
questions: {
|
|
21391
|
-
type:
|
|
22114
|
+
type: import_genai4.Type.ARRAY,
|
|
21392
22115
|
items: {
|
|
21393
|
-
type:
|
|
22116
|
+
type: import_genai4.Type.OBJECT,
|
|
21394
22117
|
properties: {
|
|
21395
|
-
objective: { type:
|
|
21396
|
-
question: { type:
|
|
22118
|
+
objective: { type: import_genai4.Type.STRING, description: "The specific learning objective tested by this question" },
|
|
22119
|
+
question: { type: import_genai4.Type.STRING, description: "The multiple-choice question text" },
|
|
21397
22120
|
options: {
|
|
21398
|
-
type:
|
|
21399
|
-
items: { type:
|
|
22121
|
+
type: import_genai4.Type.ARRAY,
|
|
22122
|
+
items: { type: import_genai4.Type.STRING },
|
|
21400
22123
|
description: "Exactly 4 options, including letter prefix like 'A) ...', 'B) ...'"
|
|
21401
22124
|
},
|
|
21402
|
-
correctAnswer: { type:
|
|
22125
|
+
correctAnswer: { type: import_genai4.Type.STRING, description: "The correct option (must exactly match one of the string options in the options array)" }
|
|
21403
22126
|
},
|
|
21404
22127
|
required: ["objective", "question", "options", "correctAnswer"]
|
|
21405
22128
|
}
|
|
@@ -21471,7 +22194,7 @@ Generate the response in the specified JSON schema.`;
|
|
|
21471
22194
|
const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
|
|
21472
22195
|
const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
|
|
21473
22196
|
if (!asb || !ast) throw new Error("Submission or assignment not found");
|
|
21474
|
-
const ai = new
|
|
22197
|
+
const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
21475
22198
|
let grade = { score: 0, feedback: "" };
|
|
21476
22199
|
let isMcqQuiz = false;
|
|
21477
22200
|
let autoScore = null;
|
|
@@ -21554,7 +22277,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
21554
22277
|
}
|
|
21555
22278
|
|
|
21556
22279
|
// server/routes/schedules.ts
|
|
21557
|
-
var
|
|
22280
|
+
var import_genai5 = require("@google/genai");
|
|
21558
22281
|
function registerSchedulesRoutes(ctx) {
|
|
21559
22282
|
const {
|
|
21560
22283
|
app,
|
|
@@ -21841,7 +22564,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
21841
22564
|
console.warn(`[OCR Error] GEMINI_API_KEY is not configured`);
|
|
21842
22565
|
return res.status(500).json({ error: lang === "zh" ? "\u672A\u914D\uFFFD? AI \u670D\u52A1\u3002\u8BF7\u5728\u7CFB\u7EDF\u8BBE\u7F6E\u4E2D\u6DFB\u52A0 AI Provider \u6216\u914D\uFFFD? GEMINI_API_KEY\uFFFD?" : "No AI provider configured. Please add an AI Provider in settings or set GEMINI_API_KEY." });
|
|
21843
22566
|
}
|
|
21844
|
-
const ai = new
|
|
22567
|
+
const ai = new import_genai5.GoogleGenAI({ apiKey: geminiKey });
|
|
21845
22568
|
const response = await ai.models.generateContent({
|
|
21846
22569
|
model: "gemini-2.5-flash",
|
|
21847
22570
|
contents: [{
|
|
@@ -21886,7 +22609,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
21886
22609
|
|
|
21887
22610
|
// server/routes/grading.ts
|
|
21888
22611
|
init_interfaces();
|
|
21889
|
-
var
|
|
22612
|
+
var import_genai6 = require("@google/genai");
|
|
21890
22613
|
function registerGradingRoutes(ctx) {
|
|
21891
22614
|
const {
|
|
21892
22615
|
app,
|
|
@@ -22454,7 +23177,7 @@ ${examsText}
|
|
|
22454
23177
|
if (!geminiKey) {
|
|
22455
23178
|
return res.status(500).json({ error: "AI provider is not configured and GEMINI_API_KEY is missing." });
|
|
22456
23179
|
}
|
|
22457
|
-
const ai = new
|
|
23180
|
+
const ai = new import_genai6.GoogleGenAI({ apiKey: geminiKey });
|
|
22458
23181
|
const response = await ai.models.generateContent({
|
|
22459
23182
|
model: "gemini-2.5-flash",
|
|
22460
23183
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
@@ -22809,798 +23532,411 @@ function registerPluginsRoutes(ctx) {
|
|
|
22809
23532
|
const zipPath = import_path13.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
|
|
22810
23533
|
if (import_fs12.default.existsSync(zipPath)) {
|
|
22811
23534
|
zipBuffer = import_fs12.default.readFileSync(zipPath);
|
|
22812
|
-
} else {
|
|
22813
|
-
return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
|
|
22814
|
-
}
|
|
22815
|
-
}
|
|
22816
|
-
const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
22817
|
-
targetPluginId,
|
|
22818
|
-
allowDowngrade: false
|
|
22819
|
-
});
|
|
22820
|
-
res.json({
|
|
22821
|
-
success: true,
|
|
22822
|
-
updated: true,
|
|
22823
|
-
pluginId: result.pluginId,
|
|
22824
|
-
manifest: result.manifest,
|
|
22825
|
-
oldVersion: result.oldVersion,
|
|
22826
|
-
newVersion: result.newVersion || "1.2.0",
|
|
22827
|
-
wasActive: result.wasActive
|
|
22828
|
-
});
|
|
22829
|
-
} catch (err) {
|
|
22830
|
-
console.error(err);
|
|
22831
|
-
res.status(500).json({ success: false, error: err.message });
|
|
22832
|
-
}
|
|
22833
|
-
});
|
|
22834
|
-
app.get("/api/plugins/:id(*)/contributions", (req, res) => {
|
|
22835
|
-
try {
|
|
22836
|
-
const rawId = decodeURIComponent(req.params.id);
|
|
22837
|
-
const summary = kernelContainer.pluginHost.listContributions(rawId);
|
|
22838
|
-
res.json({ success: true, result: summary });
|
|
22839
|
-
} catch (e) {
|
|
22840
|
-
res.status(500).json({ success: false, error: e.message });
|
|
22841
|
-
}
|
|
22842
|
-
});
|
|
22843
|
-
app.get("/api/plugins/:id(*)/config", (req, res) => {
|
|
22844
|
-
try {
|
|
22845
|
-
const rawId = decodeURIComponent(req.params.id);
|
|
22846
|
-
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
22847
|
-
const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
|
|
22848
|
-
if (!row) {
|
|
22849
|
-
return res.status(404).json({ success: false, error: "Plugin not found" });
|
|
22850
|
-
}
|
|
22851
|
-
const manifest = JSON.parse(row.manifest);
|
|
22852
|
-
res.json({
|
|
22853
|
-
success: true,
|
|
22854
|
-
result: {
|
|
22855
|
-
schema: manifest.configuration?.properties ?? {},
|
|
22856
|
-
values: kernelContainer.pluginHost.getPluginConfig(pluginId, manifest)
|
|
22857
|
-
}
|
|
22858
|
-
});
|
|
22859
|
-
} catch (e) {
|
|
22860
|
-
res.status(500).json({ success: false, error: e.message });
|
|
22861
|
-
}
|
|
22862
|
-
});
|
|
22863
|
-
app.post("/api/plugins/:id(*)/config", (req, res) => {
|
|
22864
|
-
try {
|
|
22865
|
-
const rawId = decodeURIComponent(req.params.id);
|
|
22866
|
-
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
22867
|
-
const updates = req.body;
|
|
22868
|
-
if (!updates || typeof updates !== "object") {
|
|
22869
|
-
return res.status(400).json({ success: false, error: "Body must be an object of key-value pairs" });
|
|
22870
|
-
}
|
|
22871
|
-
const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
|
|
22872
|
-
if (!row) {
|
|
22873
|
-
return res.status(404).json({ success: false, error: "Plugin not found" });
|
|
22874
|
-
}
|
|
22875
|
-
const manifest = JSON.parse(row.manifest);
|
|
22876
|
-
kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
|
|
22877
|
-
res.json({ success: true });
|
|
22878
|
-
} catch (e) {
|
|
22879
|
-
res.status(500).json({ success: false, error: e.message });
|
|
22880
|
-
}
|
|
22881
|
-
});
|
|
22882
|
-
app.post("/api/plugins/:id(*)/toggle", async (req, res) => {
|
|
22883
|
-
try {
|
|
22884
|
-
const rawId = decodeURIComponent(req.params.id);
|
|
22885
|
-
const cmd = kernelContainer.commandBus.createCommand(
|
|
22886
|
-
"plugin.toggle",
|
|
22887
|
-
{ pluginId: rawId },
|
|
22888
|
-
getActorId(req)
|
|
22889
|
-
);
|
|
22890
|
-
const result = await kernelContainer.commandBus.execute(cmd);
|
|
22891
|
-
res.json(result);
|
|
22892
|
-
} catch (err) {
|
|
22893
|
-
res.status(500).json({ success: false, error: err.message });
|
|
22894
|
-
}
|
|
22895
|
-
});
|
|
22896
|
-
app.delete("/api/plugins/:id(*)", async (req, res) => {
|
|
22897
|
-
try {
|
|
22898
|
-
const rawId = decodeURIComponent(req.params.id);
|
|
22899
|
-
const cmd = kernelContainer.commandBus.createCommand(
|
|
22900
|
-
"plugin.uninstall",
|
|
22901
|
-
{ pluginId: rawId },
|
|
22902
|
-
getActorId(req)
|
|
22903
|
-
);
|
|
22904
|
-
const result = await kernelContainer.commandBus.execute(cmd);
|
|
22905
|
-
res.json(result);
|
|
22906
|
-
} catch (err) {
|
|
22907
|
-
res.status(500).json({ success: false, error: err.message });
|
|
22908
|
-
}
|
|
22909
|
-
});
|
|
22910
|
-
app.get("/api/plugins/:id(*)", async (req, res) => {
|
|
22911
|
-
try {
|
|
22912
|
-
const rawId = decodeURIComponent(req.params.id);
|
|
22913
|
-
const cmd = kernelContainer.commandBus.createCommand(
|
|
22914
|
-
"plugin.info",
|
|
22915
|
-
{ pluginId: rawId },
|
|
22916
|
-
getActorId(req)
|
|
22917
|
-
);
|
|
22918
|
-
const result = await kernelContainer.commandBus.execute(cmd);
|
|
22919
|
-
res.json(result);
|
|
22920
|
-
} catch (err) {
|
|
22921
|
-
res.status(404).json({ success: false, error: err.message });
|
|
22922
|
-
}
|
|
22923
|
-
});
|
|
22924
|
-
app.post("/api/plugins", async (req, res) => {
|
|
22925
|
-
try {
|
|
22926
|
-
const { sourceCode } = req.body;
|
|
22927
|
-
const cmd = kernelContainer.commandBus.createCommand(
|
|
22928
|
-
"plugin.install",
|
|
22929
|
-
{ sourceCode },
|
|
22930
|
-
getActorId(req)
|
|
22931
|
-
);
|
|
22932
|
-
const result = await kernelContainer.commandBus.execute(cmd);
|
|
22933
|
-
res.json(result);
|
|
22934
|
-
} catch (err) {
|
|
22935
|
-
console.error(err);
|
|
22936
|
-
res.status(500).json({ success: false, error: err.message });
|
|
22937
|
-
}
|
|
22938
|
-
});
|
|
22939
|
-
app.post("/api/plugins/upload-zip", async (req, res) => {
|
|
22940
|
-
try {
|
|
22941
|
-
const { base64Data, filename, executionMode } = req.body;
|
|
22942
|
-
const cmd = kernelContainer.commandBus.createCommand(
|
|
22943
|
-
"plugin.install_zip",
|
|
22944
|
-
{ base64Data, filename, executionMode },
|
|
22945
|
-
getActorId(req)
|
|
22946
|
-
);
|
|
22947
|
-
const result = await kernelContainer.commandBus.execute(cmd);
|
|
22948
|
-
res.json(result);
|
|
22949
|
-
} catch (err) {
|
|
22950
|
-
console.error(err);
|
|
22951
|
-
res.status(500).json({ success: false, error: err.message });
|
|
22952
|
-
}
|
|
22953
|
-
});
|
|
22954
|
-
app.post("/api/plugins/upload-zip-raw", import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
|
|
22955
|
-
try {
|
|
22956
|
-
const zipBuffer = req.body;
|
|
22957
|
-
const filename = req.headers["x-filename"] ? decodeURIComponent(req.headers["x-filename"]) : "plugin.zip";
|
|
22958
|
-
const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
|
|
22959
|
-
const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
|
|
22960
|
-
const modeHeader = String(req.headers["x-install-mode"] || "install").toLowerCase();
|
|
22961
|
-
const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
|
|
22962
|
-
const targetPluginId = req.headers["x-target-plugin-id"] ? decodeURIComponent(String(req.headers["x-target-plugin-id"])) : void 0;
|
|
22963
|
-
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
22964
|
-
return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
|
|
22965
|
-
}
|
|
22966
|
-
if (modeHeader === "update") {
|
|
22967
|
-
const result2 = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
22968
|
-
targetPluginId,
|
|
22969
|
-
executionMode,
|
|
22970
|
-
allowDowngrade
|
|
22971
|
-
});
|
|
22972
|
-
return res.json({
|
|
22973
|
-
success: true,
|
|
22974
|
-
updated: true,
|
|
22975
|
-
pluginId: result2.pluginId,
|
|
22976
|
-
manifest: result2.manifest,
|
|
22977
|
-
oldVersion: result2.oldVersion,
|
|
22978
|
-
newVersion: result2.newVersion,
|
|
22979
|
-
wasActive: result2.wasActive,
|
|
22980
|
-
filename
|
|
22981
|
-
});
|
|
22982
|
-
}
|
|
22983
|
-
const result = await kernelContainer.pluginDistributionManager.installFromZip(zipBuffer, executionMode);
|
|
22984
|
-
res.json({
|
|
22985
|
-
success: true,
|
|
22986
|
-
updated: false,
|
|
22987
|
-
pluginId: result.pluginId,
|
|
22988
|
-
manifest: result.manifest,
|
|
22989
|
-
filename
|
|
22990
|
-
});
|
|
22991
|
-
} catch (err) {
|
|
22992
|
-
console.error(err);
|
|
22993
|
-
res.status(500).json({ success: false, error: err.message });
|
|
22994
|
-
}
|
|
22995
|
-
});
|
|
22996
|
-
app.post(
|
|
22997
|
-
"/api/plugins/:id(*)/update-zip-raw",
|
|
22998
|
-
import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }),
|
|
22999
|
-
async (req, res) => {
|
|
23000
|
-
try {
|
|
23001
|
-
const targetPluginId = decodeURIComponent(req.params.id);
|
|
23002
|
-
const zipBuffer = req.body;
|
|
23003
|
-
const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
|
|
23004
|
-
const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
|
|
23005
|
-
const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
|
|
23006
|
-
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
23007
|
-
return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
|
|
23008
|
-
}
|
|
23009
|
-
const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23010
|
-
targetPluginId,
|
|
23011
|
-
executionMode,
|
|
23012
|
-
allowDowngrade
|
|
23013
|
-
});
|
|
23014
|
-
res.json({
|
|
23015
|
-
success: true,
|
|
23016
|
-
updated: true,
|
|
23017
|
-
pluginId: result.pluginId,
|
|
23018
|
-
manifest: result.manifest,
|
|
23019
|
-
oldVersion: result.oldVersion,
|
|
23020
|
-
newVersion: result.newVersion,
|
|
23021
|
-
wasActive: result.wasActive
|
|
23022
|
-
});
|
|
23023
|
-
} catch (err) {
|
|
23024
|
-
console.error(err);
|
|
23025
|
-
res.status(500).json({ success: false, error: err.message });
|
|
23026
|
-
}
|
|
23027
|
-
}
|
|
23028
|
-
);
|
|
23029
|
-
app.post("/api/plugins/execute-command", async (req, res) => {
|
|
23030
|
-
try {
|
|
23031
|
-
const { type, payload } = req.body;
|
|
23032
|
-
if (!type) {
|
|
23033
|
-
return res.status(400).json({ success: false, error: "Missing command type" });
|
|
23034
|
-
}
|
|
23035
|
-
let resolvedType = type;
|
|
23036
|
-
const bus = kernelContainer.commandBus;
|
|
23037
|
-
const handlersMap = bus.handlers;
|
|
23038
|
-
const legacyMap = bus.legacyHandlers;
|
|
23039
|
-
if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
|
|
23040
|
-
for (const map of [handlersMap, legacyMap]) {
|
|
23041
|
-
if (!map) continue;
|
|
23042
|
-
for (const [key] of map) {
|
|
23043
|
-
if (key.endsWith(":" + resolvedType) || key.endsWith("." + resolvedType)) {
|
|
23044
|
-
resolvedType = key;
|
|
23045
|
-
break;
|
|
23046
|
-
}
|
|
23047
|
-
}
|
|
23048
|
-
if (resolvedType !== type) break;
|
|
23049
|
-
}
|
|
23050
|
-
}
|
|
23051
|
-
if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
|
|
23052
|
-
console.error("[execute-command] Handler NOT FOUND for type:", resolvedType);
|
|
23053
|
-
console.error(
|
|
23054
|
-
"[execute-command] Registered handlers:",
|
|
23055
|
-
[...handlersMap?.keys?.() ?? []].join(", ") || "(none)"
|
|
23056
|
-
);
|
|
23057
|
-
const matching = [...handlersMap?.keys?.() ?? []].filter((k) => k.includes("courseware"));
|
|
23058
|
-
console.error("[execute-command] Matching courseware keys:", matching.join(", ") || "(none)");
|
|
23059
|
-
}
|
|
23060
|
-
const cmd = await kernelContainer.commandBus.createCommand(
|
|
23061
|
-
resolvedType,
|
|
23062
|
-
payload ?? {},
|
|
23063
|
-
getActorId(req)
|
|
23064
|
-
);
|
|
23065
|
-
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23066
|
-
res.json({ success: true, result });
|
|
23535
|
+
} else {
|
|
23536
|
+
return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
|
|
23537
|
+
}
|
|
23538
|
+
}
|
|
23539
|
+
const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23540
|
+
targetPluginId,
|
|
23541
|
+
allowDowngrade: false
|
|
23542
|
+
});
|
|
23543
|
+
res.json({
|
|
23544
|
+
success: true,
|
|
23545
|
+
updated: true,
|
|
23546
|
+
pluginId: result.pluginId,
|
|
23547
|
+
manifest: result.manifest,
|
|
23548
|
+
oldVersion: result.oldVersion,
|
|
23549
|
+
newVersion: result.newVersion || "1.2.0",
|
|
23550
|
+
wasActive: result.wasActive
|
|
23551
|
+
});
|
|
23067
23552
|
} catch (err) {
|
|
23068
|
-
console.error(
|
|
23553
|
+
console.error(err);
|
|
23069
23554
|
res.status(500).json({ success: false, error: err.message });
|
|
23070
23555
|
}
|
|
23071
23556
|
});
|
|
23072
|
-
app.get("/api/
|
|
23557
|
+
app.get("/api/plugins/:id(*)/contributions", (req, res) => {
|
|
23073
23558
|
try {
|
|
23074
|
-
const
|
|
23075
|
-
const
|
|
23076
|
-
|
|
23077
|
-
api_key: maskApiKey(decryptApiKey(p.api_key || ""))
|
|
23078
|
-
}));
|
|
23079
|
-
res.json(masked);
|
|
23559
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23560
|
+
const summary = kernelContainer.pluginHost.listContributions(rawId);
|
|
23561
|
+
res.json({ success: true, result: summary });
|
|
23080
23562
|
} catch (e) {
|
|
23081
|
-
res.status(500).json({ error: e.message });
|
|
23563
|
+
res.status(500).json({ success: false, error: e.message });
|
|
23082
23564
|
}
|
|
23083
23565
|
});
|
|
23084
|
-
app.
|
|
23566
|
+
app.get("/api/plugins/:id(*)/config", (req, res) => {
|
|
23085
23567
|
try {
|
|
23086
|
-
const
|
|
23087
|
-
|
|
23088
|
-
|
|
23568
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23569
|
+
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
23570
|
+
const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
|
|
23571
|
+
if (!row) {
|
|
23572
|
+
return res.status(404).json({ success: false, error: "Plugin not found" });
|
|
23089
23573
|
}
|
|
23090
|
-
const
|
|
23091
|
-
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
|
|
23574
|
+
const manifest = JSON.parse(row.manifest);
|
|
23575
|
+
res.json({
|
|
23576
|
+
success: true,
|
|
23577
|
+
result: {
|
|
23578
|
+
schema: manifest.configuration?.properties ?? {},
|
|
23579
|
+
values: kernelContainer.pluginHost.getPluginConfig(pluginId, manifest)
|
|
23580
|
+
}
|
|
23581
|
+
});
|
|
23095
23582
|
} catch (e) {
|
|
23096
|
-
res.status(500).json({ error: e.message });
|
|
23583
|
+
res.status(500).json({ success: false, error: e.message });
|
|
23097
23584
|
}
|
|
23098
23585
|
});
|
|
23099
|
-
app.
|
|
23586
|
+
app.post("/api/plugins/:id(*)/config", (req, res) => {
|
|
23100
23587
|
try {
|
|
23101
|
-
const
|
|
23102
|
-
|
|
23103
|
-
|
|
23588
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23589
|
+
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
23590
|
+
const updates = req.body;
|
|
23591
|
+
if (!updates || typeof updates !== "object") {
|
|
23592
|
+
return res.status(400).json({ success: false, error: "Body must be an object of key-value pairs" });
|
|
23104
23593
|
}
|
|
23105
|
-
const
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
finalKey = encryptApiKey(api_key);
|
|
23109
|
-
} else {
|
|
23110
|
-
const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ?").get(req.params.id);
|
|
23111
|
-
finalKey = existing?.api_key || "";
|
|
23594
|
+
const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
|
|
23595
|
+
if (!row) {
|
|
23596
|
+
return res.status(404).json({ success: false, error: "Plugin not found" });
|
|
23112
23597
|
}
|
|
23113
|
-
|
|
23598
|
+
const manifest = JSON.parse(row.manifest);
|
|
23599
|
+
kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
|
|
23114
23600
|
res.json({ success: true });
|
|
23115
23601
|
} catch (e) {
|
|
23116
|
-
res.status(500).json({ error: e.message });
|
|
23602
|
+
res.status(500).json({ success: false, error: e.message });
|
|
23117
23603
|
}
|
|
23118
23604
|
});
|
|
23119
|
-
app.
|
|
23605
|
+
app.post("/api/plugins/:id(*)/toggle", async (req, res) => {
|
|
23120
23606
|
try {
|
|
23121
|
-
|
|
23122
|
-
|
|
23123
|
-
|
|
23124
|
-
|
|
23607
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23608
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23609
|
+
"plugin.toggle",
|
|
23610
|
+
{ pluginId: rawId },
|
|
23611
|
+
getActorId(req)
|
|
23612
|
+
);
|
|
23613
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23614
|
+
res.json(result);
|
|
23615
|
+
} catch (err) {
|
|
23616
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23125
23617
|
}
|
|
23126
23618
|
});
|
|
23127
|
-
app.
|
|
23619
|
+
app.delete("/api/plugins/:id(*)", async (req, res) => {
|
|
23128
23620
|
try {
|
|
23129
|
-
const
|
|
23130
|
-
|
|
23131
|
-
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
res.
|
|
23621
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23622
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23623
|
+
"plugin.uninstall",
|
|
23624
|
+
{ pluginId: rawId },
|
|
23625
|
+
getActorId(req)
|
|
23626
|
+
);
|
|
23627
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23628
|
+
res.json(result);
|
|
23629
|
+
} catch (err) {
|
|
23630
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23137
23631
|
}
|
|
23138
23632
|
});
|
|
23139
|
-
app.
|
|
23633
|
+
app.get("/api/plugins/:id(*)", async (req, res) => {
|
|
23140
23634
|
try {
|
|
23141
|
-
const
|
|
23142
|
-
kernelContainer.
|
|
23143
|
-
|
|
23144
|
-
|
|
23145
|
-
|
|
23146
|
-
|
|
23147
|
-
|
|
23148
|
-
|
|
23149
|
-
|
|
23150
|
-
|
|
23151
|
-
res.status(500).json({ error: e.message });
|
|
23635
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23636
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23637
|
+
"plugin.info",
|
|
23638
|
+
{ pluginId: rawId },
|
|
23639
|
+
getActorId(req)
|
|
23640
|
+
);
|
|
23641
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23642
|
+
res.json(result);
|
|
23643
|
+
} catch (err) {
|
|
23644
|
+
res.status(404).json({ success: false, error: err.message });
|
|
23152
23645
|
}
|
|
23153
23646
|
});
|
|
23154
|
-
app.post("/api/
|
|
23647
|
+
app.post("/api/plugins", async (req, res) => {
|
|
23155
23648
|
try {
|
|
23156
|
-
const {
|
|
23157
|
-
|
|
23158
|
-
|
|
23159
|
-
|
|
23160
|
-
|
|
23161
|
-
if (providedKey && providedKey.includes("****")) {
|
|
23162
|
-
const existing = kernelContainer.db.prepare(
|
|
23163
|
-
"SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? LIMIT 1"
|
|
23164
|
-
).get(api_url, model_name);
|
|
23165
|
-
api_key = existing ? decryptApiKey(existing.api_key) : "";
|
|
23166
|
-
} else if (providedKey) {
|
|
23167
|
-
api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
|
|
23168
|
-
}
|
|
23169
|
-
const controller = new AbortController();
|
|
23170
|
-
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
23171
|
-
let cleanUrl = api_url.trim();
|
|
23172
|
-
if (!cleanUrl.endsWith("/chat/completions")) {
|
|
23173
|
-
cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
|
|
23174
|
-
}
|
|
23175
|
-
const response = await fetch(cleanUrl, {
|
|
23176
|
-
method: "POST",
|
|
23177
|
-
headers: {
|
|
23178
|
-
"Content-Type": "application/json",
|
|
23179
|
-
"Authorization": `Bearer ${api_key || ""}`
|
|
23180
|
-
},
|
|
23181
|
-
body: JSON.stringify({
|
|
23182
|
-
model: model_name,
|
|
23183
|
-
messages: [{ role: "user", content: "Say connected" }],
|
|
23184
|
-
max_tokens: 5
|
|
23185
|
-
}),
|
|
23186
|
-
signal: controller.signal
|
|
23187
|
-
});
|
|
23188
|
-
clearTimeout(timeoutId);
|
|
23189
|
-
const responseText = await response.text();
|
|
23190
|
-
if (response.ok) {
|
|
23191
|
-
res.json({ success: true, message: "Successfully connected and received response." });
|
|
23192
|
-
} else {
|
|
23193
|
-
res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
|
|
23194
|
-
}
|
|
23195
|
-
} catch (e) {
|
|
23196
|
-
res.status(500).json({ success: false, error: `Connection failed: ${e.message}` });
|
|
23197
|
-
}
|
|
23198
|
-
});
|
|
23199
|
-
}
|
|
23200
|
-
|
|
23201
|
-
// server.ts
|
|
23202
|
-
import_dotenv.default.config();
|
|
23203
|
-
if (!process.env.NODE_ENV) {
|
|
23204
|
-
const isCjs = typeof __filename !== "undefined" && __filename.endsWith(".cjs");
|
|
23205
|
-
const isDist = process.cwd().endsWith("/dist") || typeof __dirname !== "undefined" && __dirname.includes("/dist") || typeof __filename !== "undefined" && __filename.includes("/dist");
|
|
23206
|
-
if (isCjs || isDist) {
|
|
23207
|
-
process.env.NODE_ENV = "production";
|
|
23208
|
-
}
|
|
23209
|
-
}
|
|
23210
|
-
var MF_REMOTE_CACHE = /* @__PURE__ */ new Map();
|
|
23211
|
-
var lessonActiveSegments = /* @__PURE__ */ new Map();
|
|
23212
|
-
var buildAgentSystemInstruction = (lang, currentLessonId) => {
|
|
23213
|
-
let systemInstruction = lang === "zh" ? "\u4F60\u662F\u4E00\u4E2A\u6559\u80B2\u7CFB\u7EDF\u5E95\u5C42\u7684 OS Agent\u3002\u4F60\u9700\u8981\u7406\u89E3\u8001\u5E08\u7684\u6307\u4EE4\uFF0C\u5E76\u8C03\u7528\u53EF\u7528\u7684\u5DE5\u5177\uFF08\u547D\u4EE4\uFF09\u53BB\u6267\u884C\u8FD9\u4E9B\u64CD\u4F5C\u3002\u5982\u679C\u8001\u5E08\u8BA9\u4F60\u521B\u5EFA\u4E00\u8282\u8BFE\uFF0C\u8BF7\u52A1\u5FC5\u5229\u7528\u5DE5\u5177\u751F\u6210\u8BE6\u7EC6\u7684\u521D\u59CB\u8BFE\u7A0B\u5185\u5BB9\u3002\u5982\u679C\u8001\u5E08\u8981\u6C42\u7BA1\u7406\u8FDB\u7A0B/\u4EFB\u52A1\uFF0C\u8BF7\u4F7F\u7528 process.spawn, process.kill, process.list\u3002\u5982\u679C\u9700\u5B58\u50A8\u6587\u4EF6\u3001\u7D20\u6750\u6216\u521B\u5EFA\u76EE\u5F55\uFF0C\u8BF7\u4F7F\u7528 vfs.* \u5E76\u5728\u9700\u8981\u65F6\u7BA1\u7406\u73ED\u7EA7\u548C\u5B66\u751F\u3002\u4F60\u652F\u6301\u901A\u8FC7 class_create \u521B\u5EFA\u73ED\u7EA7, student_create \u521B\u5EFA\u5B66\u751F, class_add_student \u5C06\u5B66\u751F\u52A0\u5165\u73ED\u7EA7\u3002\u5F53\u8001\u5E08\u8981\u6C42\u4ECE\u63D0\u4F9B\u7684\u6570\u636E\uFF08\u5982CSV\u3001JSON\u3001Markdown\u6216\u5BF9\u8BDD\u4E2D\uFF09\u521B\u5EFA\u73ED\u7EA7\u6216\u5B66\u751F\u65F6\uFF0C\u8BF7\u4F9D\u6B21\u53D1\u51FA\u8FD9\u4E9B\u6307\u4EE4\u3002\u5982\u679C\u4E0A\u4E00\u9636\u6BB5\u8FD4\u56DE\u4E86\u521B\u5EFA\u6210\u529F\u7684\u73ED\u7EA7ID\u6216\u5B66\u751FID\uFF0C\u4F60\u9700\u8981\u5728\u540E\u7EED\uFFFD? functionCall \u4E2D\u5F15\u7528\u8FD9\u4E9BID\uFF08\u4F8B\u5982\uFF1A\u628A\u521A\u521B\u5EFA\u7684\u5B66\u751FID\u52A0\u5165\u5230\u521A\u521B\u5EFA\u7684\u73ED\u7EA7ID\u4E2D\uFF09\u3002\u901A\u8FC7\u5F80\u590D\u7684\u5DE5\u5177\u8C03\u7528\uFF0C\u4F60\u53EF\u4EE5\u81EA\u52A8\u5B8C\u6210\u5B8C\u6574\u7684\u6D41\u7A0B\uFFFD?" : "You are an educational OS kernel agent. You interpret teacher instructions and use your available tools (commands) to execute them. If the teacher asks to create a lesson, always generate some detailed initial content for it. If the teacher asks to spawn or kill processes, use process tools. Use vfs tools to store assets, and manage classes/students as necessary. You support class_create, student_create, class_add_student. Always use tool chaining if you need to create a class and enroll students: first call class_create/student_create, receive their returned IDs, and then call class_add_student in the next turn. Always answer with a helpful summary.";
|
|
23214
|
-
if (currentLessonId) {
|
|
23215
|
-
systemInstruction += `
|
|
23216
|
-
[Context] The current selected lesson ID is "${currentLessonId}". Use this ID if the teacher's instruction is about modifying or adding to the current lesson.
|
|
23217
|
-
|
|
23218
|
-
Available tools (functions) can be used multiple times in sequence if needed.`;
|
|
23219
|
-
}
|
|
23220
|
-
return systemInstruction;
|
|
23221
|
-
};
|
|
23222
|
-
var buildAgentFinalMessage = (message, attachments) => {
|
|
23223
|
-
let finalMessage = message;
|
|
23224
|
-
if (attachments && Array.isArray(attachments) && attachments.length > 0) {
|
|
23225
|
-
finalMessage += "\n\n[Attached Reference Files]";
|
|
23226
|
-
attachments.forEach((file, index) => {
|
|
23227
|
-
if (file.name.endsWith(".zip") || file.content.startsWith("data:application/zip") || file.content.length > 5e3) {
|
|
23228
|
-
finalMessage += `
|
|
23229
|
-
|
|
23230
|
-
Filename: "${file.name}"
|
|
23231
|
-
Content: "ATTACHMENT_BASE64:${index}"`;
|
|
23232
|
-
} else {
|
|
23233
|
-
finalMessage += `
|
|
23234
|
-
|
|
23235
|
-
Filename: "${file.name}"
|
|
23236
|
-
Content:
|
|
23237
|
-
"""
|
|
23238
|
-
${file.content}
|
|
23239
|
-
"""`;
|
|
23240
|
-
}
|
|
23241
|
-
});
|
|
23242
|
-
}
|
|
23243
|
-
return finalMessage;
|
|
23244
|
-
};
|
|
23245
|
-
var normalizeToolSchema = (schema) => {
|
|
23246
|
-
if (!schema || typeof schema !== "object") return schema;
|
|
23247
|
-
if (Array.isArray(schema)) return schema.map(normalizeToolSchema);
|
|
23248
|
-
const normalized = {};
|
|
23249
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
23250
|
-
if (key === "type" && typeof value === "string") {
|
|
23251
|
-
const typeMap = {
|
|
23252
|
-
OBJECT: "object",
|
|
23253
|
-
STRING: "string",
|
|
23254
|
-
ARRAY: "array",
|
|
23255
|
-
INTEGER: "integer",
|
|
23256
|
-
NUMBER: "number",
|
|
23257
|
-
BOOLEAN: "boolean"
|
|
23258
|
-
};
|
|
23259
|
-
normalized.type = typeMap[value.toUpperCase()] || value.toLowerCase();
|
|
23260
|
-
continue;
|
|
23261
|
-
}
|
|
23262
|
-
if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
23263
|
-
normalized.properties = Object.fromEntries(
|
|
23264
|
-
Object.entries(value).map(([propKey, propSchema]) => [propKey, normalizeToolSchema(propSchema)])
|
|
23649
|
+
const { sourceCode } = req.body;
|
|
23650
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23651
|
+
"plugin.install",
|
|
23652
|
+
{ sourceCode },
|
|
23653
|
+
getActorId(req)
|
|
23265
23654
|
);
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
}
|
|
23272
|
-
normalized[key] = value;
|
|
23273
|
-
}
|
|
23274
|
-
return normalized;
|
|
23275
|
-
};
|
|
23276
|
-
var buildOpenAITools = () => {
|
|
23277
|
-
const actions = kernelContainer.actionRegistry.getAllActions();
|
|
23278
|
-
return actions.map((action) => ({
|
|
23279
|
-
type: "function",
|
|
23280
|
-
function: {
|
|
23281
|
-
name: action.commandType.replace(/[^a-zA-Z0-9_\-]/g, "_"),
|
|
23282
|
-
description: action.description,
|
|
23283
|
-
parameters: normalizeToolSchema(action.inputSchema)
|
|
23655
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23656
|
+
res.json(result);
|
|
23657
|
+
} catch (err) {
|
|
23658
|
+
console.error(err);
|
|
23659
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23284
23660
|
}
|
|
23285
|
-
})
|
|
23286
|
-
|
|
23287
|
-
var executeAgentToolCall = async (toolName, args, allExecutedTools, callerRole, currentLessonId) => {
|
|
23288
|
-
const actionDesc = kernelContainer.actionRegistry.getActionByToolName(toolName);
|
|
23289
|
-
let actionResult;
|
|
23290
|
-
const isAdmin = callerRole === "administrator";
|
|
23291
|
-
const actorId = isAdmin ? "user-frontend" : "agent-system-0";
|
|
23292
|
-
const metadata = isAdmin ? { approved: true } : void 0;
|
|
23293
|
-
if (actionDesc) {
|
|
23294
|
-
const cmd = kernelContainer.commandBus.createCommand(
|
|
23295
|
-
actionDesc.commandType,
|
|
23296
|
-
args,
|
|
23297
|
-
actorId,
|
|
23298
|
-
metadata
|
|
23299
|
-
);
|
|
23661
|
+
});
|
|
23662
|
+
app.post("/api/plugins/upload-zip", async (req, res) => {
|
|
23300
23663
|
try {
|
|
23301
|
-
const
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
try {
|
|
23310
|
-
const dataObj = JSON.parse(row.data);
|
|
23311
|
-
if (!dataObj.segmentId) {
|
|
23312
|
-
dataObj.segmentId = activeSeg;
|
|
23313
|
-
kernelContainer.db.prepare("UPDATE whiteboard_elements SET data = ? WHERE id = ?").run(JSON.stringify(dataObj), cmdResult.elementId);
|
|
23314
|
-
console.log(`[Agent Tool Sync] Injected active segment "${activeSeg}" into element "${cmdResult.elementId}"`);
|
|
23315
|
-
kernelContainer.eventBus.publish({
|
|
23316
|
-
id: import_crypto13.default.randomUUID(),
|
|
23317
|
-
type: "whiteboard.element_updated",
|
|
23318
|
-
source: "agent-tool-sync",
|
|
23319
|
-
payload: { elementId: cmdResult.elementId, lessonId: currentLessonId },
|
|
23320
|
-
timestamp: Date.now(),
|
|
23321
|
-
correlationId: cmd.id
|
|
23322
|
-
}).catch((e) => console.error("[Agent Tool Sync] Failed to publish element_updated event:", e));
|
|
23323
|
-
}
|
|
23324
|
-
} catch (e) {
|
|
23325
|
-
console.error("[Agent Tool Sync] Failed to parse/update element data:", e);
|
|
23326
|
-
}
|
|
23327
|
-
}
|
|
23328
|
-
}
|
|
23329
|
-
}
|
|
23664
|
+
const { base64Data, filename, executionMode } = req.body;
|
|
23665
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23666
|
+
"plugin.install_zip",
|
|
23667
|
+
{ base64Data, filename, executionMode },
|
|
23668
|
+
getActorId(req)
|
|
23669
|
+
);
|
|
23670
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23671
|
+
res.json(result);
|
|
23330
23672
|
} catch (err) {
|
|
23331
|
-
|
|
23332
|
-
|
|
23673
|
+
console.error(err);
|
|
23674
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23333
23675
|
}
|
|
23334
|
-
}
|
|
23335
|
-
|
|
23336
|
-
|
|
23337
|
-
|
|
23338
|
-
|
|
23339
|
-
|
|
23340
|
-
|
|
23341
|
-
|
|
23342
|
-
|
|
23343
|
-
|
|
23344
|
-
|
|
23345
|
-
|
|
23346
|
-
};
|
|
23347
|
-
var runGeminiAgentChat = async (request) => {
|
|
23348
|
-
const { message, lang = "zh", currentLessonId, attachments, callerRole, history } = request;
|
|
23349
|
-
const apiKey = process.env.GEMINI_API_KEY;
|
|
23350
|
-
if (!apiKey || apiKey.trim() === "" || apiKey.trim() === "MY_GEMINI_API_KEY") {
|
|
23351
|
-
throw new Error(
|
|
23352
|
-
lang === "zh" ? "\u672A\u914D\u7F6E\u53EF\u7528\u7684 AI \u670D\u52A1\u3002\u8BF7\u5728\u7BA1\u7406\u540E\u53F0\u7684\u300CAI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u4E2D\u6DFB\u52A0\u4E00\u4E2A AI \u63D0\u4F9B\u5546\uFF08\u6216\u8BBE\u7F6E GEMINI_API_KEY \u4F5C\u4E3A\u517C\u5BB9\u56DE\u9000\uFF09\u3002" : 'No AI service is configured. Please add an AI Provider in the admin dashboard\'s "AI Provider Management" (or set `GEMINI_API_KEY` as a compatible fallback).'
|
|
23353
|
-
);
|
|
23354
|
-
}
|
|
23355
|
-
const ai = new import_genai6.GoogleGenAI({ apiKey: apiKey.trim() });
|
|
23356
|
-
const tools = kernelContainer.actionRegistry.getAgentTools();
|
|
23357
|
-
const systemInstruction = buildAgentSystemInstruction(lang, currentLessonId);
|
|
23358
|
-
const finalMessage = buildAgentFinalMessage(message, attachments);
|
|
23359
|
-
const historyContents = (history || []).map((h) => ({
|
|
23360
|
-
role: h.role === "assistant" ? "model" : "user",
|
|
23361
|
-
parts: [{ text: h.content }]
|
|
23362
|
-
}));
|
|
23363
|
-
const contents = [...historyContents, { role: "user", parts: [{ text: finalMessage }] }];
|
|
23364
|
-
let loopCount = 0;
|
|
23365
|
-
const MAX_LOOPS = 5;
|
|
23366
|
-
let finalResponseText = "";
|
|
23367
|
-
const allExecutedTools = [];
|
|
23368
|
-
while (loopCount < MAX_LOOPS) {
|
|
23369
|
-
const response = await ai.models.generateContent({
|
|
23370
|
-
model: "gemini-3.5-flash",
|
|
23371
|
-
contents,
|
|
23372
|
-
config: {
|
|
23373
|
-
systemInstruction,
|
|
23374
|
-
tools,
|
|
23375
|
-
temperature: 0.1
|
|
23676
|
+
});
|
|
23677
|
+
app.post("/api/plugins/upload-zip-raw", import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
|
|
23678
|
+
try {
|
|
23679
|
+
const zipBuffer = req.body;
|
|
23680
|
+
const filename = req.headers["x-filename"] ? decodeURIComponent(req.headers["x-filename"]) : "plugin.zip";
|
|
23681
|
+
const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
|
|
23682
|
+
const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
|
|
23683
|
+
const modeHeader = String(req.headers["x-install-mode"] || "install").toLowerCase();
|
|
23684
|
+
const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
|
|
23685
|
+
const targetPluginId = req.headers["x-target-plugin-id"] ? decodeURIComponent(String(req.headers["x-target-plugin-id"])) : void 0;
|
|
23686
|
+
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
23687
|
+
return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
|
|
23376
23688
|
}
|
|
23377
|
-
|
|
23378
|
-
|
|
23379
|
-
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
const call = part.functionCall;
|
|
23393
|
-
if (call.args && typeof call.args === "object" && attachments) {
|
|
23394
|
-
for (const key of Object.keys(call.args)) {
|
|
23395
|
-
const val = call.args[key];
|
|
23396
|
-
if (typeof val === "string" && val.startsWith("ATTACHMENT_BASE64:")) {
|
|
23397
|
-
const idx = parseInt(val.split(":")[1]);
|
|
23398
|
-
if (attachments[idx]) {
|
|
23399
|
-
call.args[key] = attachments[idx].content;
|
|
23400
|
-
}
|
|
23401
|
-
}
|
|
23402
|
-
}
|
|
23403
|
-
}
|
|
23404
|
-
const actionResult = await executeAgentToolCall(call.name, call.args, allExecutedTools, callerRole, currentLessonId);
|
|
23405
|
-
toolParts.push({
|
|
23406
|
-
functionResponse: {
|
|
23407
|
-
name: call.name,
|
|
23408
|
-
response: typeof actionResult === "object" && actionResult !== null ? actionResult : { value: actionResult }
|
|
23409
|
-
}
|
|
23689
|
+
if (modeHeader === "update") {
|
|
23690
|
+
const result2 = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23691
|
+
targetPluginId,
|
|
23692
|
+
executionMode,
|
|
23693
|
+
allowDowngrade
|
|
23694
|
+
});
|
|
23695
|
+
return res.json({
|
|
23696
|
+
success: true,
|
|
23697
|
+
updated: true,
|
|
23698
|
+
pluginId: result2.pluginId,
|
|
23699
|
+
manifest: result2.manifest,
|
|
23700
|
+
oldVersion: result2.oldVersion,
|
|
23701
|
+
newVersion: result2.newVersion,
|
|
23702
|
+
wasActive: result2.wasActive,
|
|
23703
|
+
filename
|
|
23410
23704
|
});
|
|
23411
23705
|
}
|
|
23706
|
+
const result = await kernelContainer.pluginDistributionManager.installFromZip(zipBuffer, executionMode);
|
|
23707
|
+
res.json({
|
|
23708
|
+
success: true,
|
|
23709
|
+
updated: false,
|
|
23710
|
+
pluginId: result.pluginId,
|
|
23711
|
+
manifest: result.manifest,
|
|
23712
|
+
filename
|
|
23713
|
+
});
|
|
23714
|
+
} catch (err) {
|
|
23715
|
+
console.error(err);
|
|
23716
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23412
23717
|
}
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
})
|
|
23417
|
-
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23421
|
-
|
|
23422
|
-
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
};
|
|
23427
|
-
var runOpenAIAgentChat = async (provider, request) => {
|
|
23428
|
-
const { message, lang = "zh", currentLessonId, attachments, callerRole, history } = request;
|
|
23429
|
-
const systemInstruction = buildAgentSystemInstruction(lang, currentLessonId);
|
|
23430
|
-
const finalMessage = buildAgentFinalMessage(message, attachments);
|
|
23431
|
-
const tools = buildOpenAITools();
|
|
23432
|
-
const chatUrl = buildOpenAIChatUrl(provider.api_url);
|
|
23433
|
-
const headers = {
|
|
23434
|
-
"Content-Type": "application/json"
|
|
23435
|
-
};
|
|
23436
|
-
if (provider.api_key && provider.api_key.trim()) {
|
|
23437
|
-
headers.Authorization = `Bearer ${provider.api_key.trim()}`;
|
|
23438
|
-
}
|
|
23439
|
-
const historyMessages = (history || []).map((h) => ({ role: h.role, content: h.content }));
|
|
23440
|
-
const messages = [
|
|
23441
|
-
{ role: "system", content: systemInstruction },
|
|
23442
|
-
...historyMessages,
|
|
23443
|
-
{ role: "user", content: finalMessage }
|
|
23444
|
-
];
|
|
23445
|
-
const allExecutedTools = [];
|
|
23446
|
-
let finalResponseText = "";
|
|
23447
|
-
const MAX_LOOPS = 5;
|
|
23448
|
-
let loopCount = 0;
|
|
23449
|
-
while (loopCount < MAX_LOOPS) {
|
|
23450
|
-
const response = await fetch(chatUrl, {
|
|
23451
|
-
method: "POST",
|
|
23452
|
-
headers,
|
|
23453
|
-
body: JSON.stringify({
|
|
23454
|
-
model: provider.model_name,
|
|
23455
|
-
messages,
|
|
23456
|
-
tools,
|
|
23457
|
-
tool_choice: tools.length > 0 ? "auto" : void 0,
|
|
23458
|
-
temperature: 0.1
|
|
23459
|
-
})
|
|
23460
|
-
});
|
|
23461
|
-
if (!response.ok) {
|
|
23462
|
-
const errorText = await response.text();
|
|
23463
|
-
throw new Error(`AI provider request failed (${response.status}): ${errorText || response.statusText}`);
|
|
23464
|
-
}
|
|
23465
|
-
const data = await response.json();
|
|
23466
|
-
const assistantMessage = data.choices?.[0]?.message;
|
|
23467
|
-
if (!assistantMessage) {
|
|
23468
|
-
throw new Error("AI provider returned no assistant message");
|
|
23469
|
-
}
|
|
23470
|
-
finalResponseText = typeof assistantMessage.content === "string" ? assistantMessage.content.trim() : "";
|
|
23471
|
-
const toolCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
23472
|
-
messages.push({
|
|
23473
|
-
role: "assistant",
|
|
23474
|
-
content: assistantMessage.content ?? "",
|
|
23475
|
-
tool_calls: toolCalls
|
|
23476
|
-
});
|
|
23477
|
-
if (toolCalls.length === 0) {
|
|
23478
|
-
break;
|
|
23479
|
-
}
|
|
23480
|
-
for (const call of toolCalls) {
|
|
23481
|
-
const toolName = call?.function?.name;
|
|
23482
|
-
if (!toolName) continue;
|
|
23483
|
-
let parsedArgs = {};
|
|
23484
|
-
if (typeof call?.function?.arguments === "string" && call.function.arguments.trim()) {
|
|
23485
|
-
try {
|
|
23486
|
-
parsedArgs = JSON.parse(call.function.arguments);
|
|
23487
|
-
} catch (err) {
|
|
23488
|
-
parsedArgs = {};
|
|
23718
|
+
});
|
|
23719
|
+
app.post(
|
|
23720
|
+
"/api/plugins/:id(*)/update-zip-raw",
|
|
23721
|
+
import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }),
|
|
23722
|
+
async (req, res) => {
|
|
23723
|
+
try {
|
|
23724
|
+
const targetPluginId = decodeURIComponent(req.params.id);
|
|
23725
|
+
const zipBuffer = req.body;
|
|
23726
|
+
const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
|
|
23727
|
+
const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
|
|
23728
|
+
const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
|
|
23729
|
+
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
23730
|
+
return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
|
|
23489
23731
|
}
|
|
23732
|
+
const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23733
|
+
targetPluginId,
|
|
23734
|
+
executionMode,
|
|
23735
|
+
allowDowngrade
|
|
23736
|
+
});
|
|
23737
|
+
res.json({
|
|
23738
|
+
success: true,
|
|
23739
|
+
updated: true,
|
|
23740
|
+
pluginId: result.pluginId,
|
|
23741
|
+
manifest: result.manifest,
|
|
23742
|
+
oldVersion: result.oldVersion,
|
|
23743
|
+
newVersion: result.newVersion,
|
|
23744
|
+
wasActive: result.wasActive
|
|
23745
|
+
});
|
|
23746
|
+
} catch (err) {
|
|
23747
|
+
console.error(err);
|
|
23748
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23490
23749
|
}
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
|
|
23496
|
-
|
|
23497
|
-
|
|
23750
|
+
}
|
|
23751
|
+
);
|
|
23752
|
+
app.post("/api/plugins/execute-command", async (req, res) => {
|
|
23753
|
+
try {
|
|
23754
|
+
const { type, payload } = req.body;
|
|
23755
|
+
if (!type) {
|
|
23756
|
+
return res.status(400).json({ success: false, error: "Missing command type" });
|
|
23757
|
+
}
|
|
23758
|
+
let resolvedType = type;
|
|
23759
|
+
const bus = kernelContainer.commandBus;
|
|
23760
|
+
const handlersMap = bus.handlers;
|
|
23761
|
+
const legacyMap = bus.legacyHandlers;
|
|
23762
|
+
if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
|
|
23763
|
+
for (const map of [handlersMap, legacyMap]) {
|
|
23764
|
+
if (!map) continue;
|
|
23765
|
+
for (const [key] of map) {
|
|
23766
|
+
if (key.endsWith(":" + resolvedType) || key.endsWith("." + resolvedType)) {
|
|
23767
|
+
resolvedType = key;
|
|
23768
|
+
break;
|
|
23498
23769
|
}
|
|
23499
23770
|
}
|
|
23771
|
+
if (resolvedType !== type) break;
|
|
23500
23772
|
}
|
|
23501
23773
|
}
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23507
|
-
|
|
23774
|
+
if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
|
|
23775
|
+
console.error("[execute-command] Handler NOT FOUND for type:", resolvedType);
|
|
23776
|
+
console.error(
|
|
23777
|
+
"[execute-command] Registered handlers:",
|
|
23778
|
+
[...handlersMap?.keys?.() ?? []].join(", ") || "(none)"
|
|
23779
|
+
);
|
|
23780
|
+
const matching = [...handlersMap?.keys?.() ?? []].filter((k) => k.includes("courseware"));
|
|
23781
|
+
console.error("[execute-command] Matching courseware keys:", matching.join(", ") || "(none)");
|
|
23782
|
+
}
|
|
23783
|
+
const cmd = await kernelContainer.commandBus.createCommand(
|
|
23784
|
+
resolvedType,
|
|
23785
|
+
payload ?? {},
|
|
23786
|
+
getActorId(req)
|
|
23787
|
+
);
|
|
23788
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23789
|
+
res.json({ success: true, result });
|
|
23790
|
+
} catch (err) {
|
|
23791
|
+
console.error("[execute-command]", err.message);
|
|
23792
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23508
23793
|
}
|
|
23509
|
-
loopCount++;
|
|
23510
|
-
}
|
|
23511
|
-
if (loopCount >= MAX_LOOPS && !finalResponseText) {
|
|
23512
|
-
finalResponseText = "I have executed several internal commands, but reached the iteration limit. Please review the assistant panel for the latest state.";
|
|
23513
|
-
}
|
|
23514
|
-
return {
|
|
23515
|
-
agentText: finalResponseText,
|
|
23516
|
-
toolResults: allExecutedTools
|
|
23517
|
-
};
|
|
23518
|
-
};
|
|
23519
|
-
async function startServer() {
|
|
23520
|
-
await ServerBootstrapAdapter.bootstrap({
|
|
23521
|
-
kernelContainer,
|
|
23522
|
-
environment: process.env.NODE_ENV || "development",
|
|
23523
|
-
config: { port: Number(process.env.PORT) || 9e3 }
|
|
23524
23794
|
});
|
|
23525
|
-
|
|
23526
|
-
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23795
|
+
app.get("/api/ai-providers", (req, res) => {
|
|
23796
|
+
try {
|
|
23797
|
+
const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
|
|
23798
|
+
const masked = providers.map((p) => ({
|
|
23799
|
+
...p,
|
|
23800
|
+
api_key: maskApiKey(decryptApiKey(p.api_key || ""))
|
|
23801
|
+
}));
|
|
23802
|
+
res.json(masked);
|
|
23803
|
+
} catch (e) {
|
|
23804
|
+
res.status(500).json({ error: e.message });
|
|
23530
23805
|
}
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
23806
|
+
});
|
|
23807
|
+
app.post("/api/ai-providers", (req, res) => {
|
|
23808
|
+
try {
|
|
23809
|
+
const { name, api_url, api_key, model_name } = req.body;
|
|
23810
|
+
if (!name || !api_url || !model_name) {
|
|
23811
|
+
return res.status(400).json({ error: "Missing name, api_url or model_name" });
|
|
23812
|
+
}
|
|
23813
|
+
const id = "prov_" + Date.now();
|
|
23814
|
+
const now = Date.now();
|
|
23815
|
+
const encryptedKey = api_key ? encryptApiKey(api_key) : "";
|
|
23816
|
+
kernelContainer.db.prepare("INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, name, api_url, encryptedKey, model_name, now, now);
|
|
23817
|
+
res.json({ success: true, id });
|
|
23818
|
+
} catch (e) {
|
|
23819
|
+
res.status(500).json({ error: e.message });
|
|
23535
23820
|
}
|
|
23536
|
-
}
|
|
23537
|
-
|
|
23538
|
-
|
|
23539
|
-
|
|
23540
|
-
|
|
23541
|
-
|
|
23542
|
-
|
|
23543
|
-
|
|
23544
|
-
|
|
23545
|
-
|
|
23546
|
-
|
|
23547
|
-
|
|
23548
|
-
|
|
23549
|
-
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23554
|
-
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
);
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
23570
|
-
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
);
|
|
23575
|
-
|
|
23576
|
-
|
|
23577
|
-
|
|
23578
|
-
|
|
23579
|
-
|
|
23580
|
-
|
|
23581
|
-
|
|
23582
|
-
|
|
23583
|
-
|
|
23584
|
-
|
|
23585
|
-
|
|
23586
|
-
|
|
23587
|
-
|
|
23588
|
-
|
|
23589
|
-
|
|
23590
|
-
|
|
23591
|
-
|
|
23592
|
-
|
|
23593
|
-
|
|
23594
|
-
|
|
23595
|
-
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
|
|
23821
|
+
});
|
|
23822
|
+
app.put("/api/ai-providers/:id", (req, res) => {
|
|
23823
|
+
try {
|
|
23824
|
+
const { name, api_url, api_key, model_name } = req.body;
|
|
23825
|
+
if (!name || !api_url || !model_name) {
|
|
23826
|
+
return res.status(400).json({ error: "Missing name, api_url or model_name" });
|
|
23827
|
+
}
|
|
23828
|
+
const now = Date.now();
|
|
23829
|
+
let finalKey;
|
|
23830
|
+
if (api_key && api_key.trim() !== "" && !api_key.includes("****")) {
|
|
23831
|
+
finalKey = encryptApiKey(api_key);
|
|
23832
|
+
} else {
|
|
23833
|
+
const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ?").get(req.params.id);
|
|
23834
|
+
finalKey = existing?.api_key || "";
|
|
23835
|
+
}
|
|
23836
|
+
kernelContainer.db.prepare("UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?").run(name, api_url, finalKey, model_name, now, req.params.id);
|
|
23837
|
+
res.json({ success: true });
|
|
23838
|
+
} catch (e) {
|
|
23839
|
+
res.status(500).json({ error: e.message });
|
|
23840
|
+
}
|
|
23841
|
+
});
|
|
23842
|
+
app.delete("/api/ai-providers/:id", (req, res) => {
|
|
23843
|
+
try {
|
|
23844
|
+
kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
|
|
23845
|
+
res.json({ success: true });
|
|
23846
|
+
} catch (e) {
|
|
23847
|
+
res.status(500).json({ error: e.message });
|
|
23848
|
+
}
|
|
23849
|
+
});
|
|
23850
|
+
app.get("/api/site-settings", (req, res) => {
|
|
23851
|
+
try {
|
|
23852
|
+
const row = kernelContainer.db.prepare("SELECT site_name, slogan, logo_url FROM site_settings WHERE id = ?").get("global");
|
|
23853
|
+
res.json({
|
|
23854
|
+
siteName: row?.site_name || "",
|
|
23855
|
+
slogan: row?.slogan || "",
|
|
23856
|
+
logoUrl: row?.logo_url || null
|
|
23857
|
+
});
|
|
23858
|
+
} catch (e) {
|
|
23859
|
+
res.status(500).json({ error: e.message });
|
|
23860
|
+
}
|
|
23861
|
+
});
|
|
23862
|
+
app.put("/api/site-settings", (req, res) => {
|
|
23863
|
+
try {
|
|
23864
|
+
const { siteName, slogan, logoUrl } = req.body || {};
|
|
23865
|
+
kernelContainer.db.prepare(
|
|
23866
|
+
`INSERT INTO site_settings (id, site_name, slogan, logo_url) VALUES ('global', ?, ?, ?)
|
|
23867
|
+
ON CONFLICT(id) DO UPDATE SET site_name = excluded.site_name, slogan = excluded.slogan, logo_url = excluded.logo_url`
|
|
23868
|
+
).run(siteName || "", slogan || "", logoUrl || null);
|
|
23869
|
+
res.json({
|
|
23870
|
+
success: true,
|
|
23871
|
+
siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
|
|
23872
|
+
});
|
|
23873
|
+
} catch (e) {
|
|
23874
|
+
res.status(500).json({ error: e.message });
|
|
23875
|
+
}
|
|
23876
|
+
});
|
|
23877
|
+
app.post("/api/ai-providers/test", async (req, res) => {
|
|
23878
|
+
try {
|
|
23879
|
+
const { api_url, api_key: providedKey, model_name } = req.body;
|
|
23880
|
+
if (!api_url || !model_name) {
|
|
23881
|
+
return res.status(400).json({ error: "api_url and model_name are required" });
|
|
23882
|
+
}
|
|
23883
|
+
let api_key = "";
|
|
23884
|
+
if (providedKey && providedKey.includes("****")) {
|
|
23885
|
+
const existing = kernelContainer.db.prepare(
|
|
23886
|
+
"SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? LIMIT 1"
|
|
23887
|
+
).get(api_url, model_name);
|
|
23888
|
+
api_key = existing ? decryptApiKey(existing.api_key) : "";
|
|
23889
|
+
} else if (providedKey) {
|
|
23890
|
+
api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
|
|
23891
|
+
}
|
|
23892
|
+
const controller = new AbortController();
|
|
23893
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
23894
|
+
let cleanUrl = api_url.trim();
|
|
23895
|
+
if (!cleanUrl.endsWith("/chat/completions")) {
|
|
23896
|
+
cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
|
|
23897
|
+
}
|
|
23898
|
+
const response = await fetch(cleanUrl, {
|
|
23899
|
+
method: "POST",
|
|
23900
|
+
headers: {
|
|
23901
|
+
"Content-Type": "application/json",
|
|
23902
|
+
"Authorization": `Bearer ${api_key || ""}`
|
|
23903
|
+
},
|
|
23904
|
+
body: JSON.stringify({
|
|
23905
|
+
model: model_name,
|
|
23906
|
+
messages: [{ role: "user", content: "Say connected" }],
|
|
23907
|
+
max_tokens: 5
|
|
23908
|
+
}),
|
|
23909
|
+
signal: controller.signal
|
|
23910
|
+
});
|
|
23911
|
+
clearTimeout(timeoutId);
|
|
23912
|
+
const responseText = await response.text();
|
|
23913
|
+
if (response.ok) {
|
|
23914
|
+
res.json({ success: true, message: "Successfully connected and received response." });
|
|
23915
|
+
} else {
|
|
23916
|
+
res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
|
|
23917
|
+
}
|
|
23918
|
+
} catch (e) {
|
|
23919
|
+
res.status(500).json({ success: false, error: `Connection failed: ${e.message}` });
|
|
23600
23920
|
}
|
|
23601
|
-
}
|
|
23602
|
-
|
|
23921
|
+
});
|
|
23922
|
+
}
|
|
23923
|
+
|
|
23924
|
+
// server.ts
|
|
23925
|
+
import_dotenv.default.config();
|
|
23926
|
+
if (!process.env.NODE_ENV) {
|
|
23927
|
+
const isCjs = typeof __filename !== "undefined" && __filename.endsWith(".cjs");
|
|
23928
|
+
const isDist = process.cwd().endsWith("/dist") || typeof __dirname !== "undefined" && __dirname.includes("/dist") || typeof __filename !== "undefined" && __filename.includes("/dist");
|
|
23929
|
+
if (isCjs || isDist) {
|
|
23930
|
+
process.env.NODE_ENV = "production";
|
|
23603
23931
|
}
|
|
23932
|
+
}
|
|
23933
|
+
async function startServer() {
|
|
23934
|
+
await ServerBootstrapAdapter.bootstrap({
|
|
23935
|
+
kernelContainer,
|
|
23936
|
+
environment: process.env.NODE_ENV || "development",
|
|
23937
|
+
config: { port: Number(process.env.PORT) || 9e3 }
|
|
23938
|
+
});
|
|
23939
|
+
await runStartupMigrations(kernelContainer.db);
|
|
23604
23940
|
await kernelContainer.ready;
|
|
23605
23941
|
const activityRegistry = new ActivityRegistry2();
|
|
23606
23942
|
registerOfficialActivities(activityRegistry, kernelContainer.actionRegistry);
|
|
@@ -23619,7 +23955,7 @@ async function startServer() {
|
|
|
23619
23955
|
imgSrc: ["'self'", "data:", "blob:", "https:"],
|
|
23620
23956
|
connectSrc: ["'self'", "ws:", "wss:", "https:"],
|
|
23621
23957
|
fontSrc: ["'self'", "data:", "https:", "https://fonts.gstatic.com"],
|
|
23622
|
-
frameSrc: ["'self'"],
|
|
23958
|
+
frameSrc: ["'self'", "blob:", "data:", "http://localhost", "http://127.0.0.1", "http:", "https:"],
|
|
23623
23959
|
objectSrc: ["'none'"]
|
|
23624
23960
|
}
|
|
23625
23961
|
},
|
|
@@ -23695,230 +24031,8 @@ async function startServer() {
|
|
|
23695
24031
|
registerSchedulesRoutes(ctx);
|
|
23696
24032
|
registerGradingRoutes(ctx);
|
|
23697
24033
|
registerPluginsRoutes(ctx);
|
|
23698
|
-
kernelContainer.eventBus
|
|
23699
|
-
|
|
23700
|
-
const payload = event.payload;
|
|
23701
|
-
const assignment = kernelContainer.db.prepare("SELECT title FROM assignments WHERE id = ?").get(payload.assignmentId);
|
|
23702
|
-
const assignmentTitle = assignment ? assignment.title : "Assignment";
|
|
23703
|
-
console.log(`[EventBus -> Socket.IO] Broadcasting assignment-graded-toast to student ${payload.studentId}`);
|
|
23704
|
-
io.emit("assignment-graded-toast", {
|
|
23705
|
-
assignmentId: payload.assignmentId,
|
|
23706
|
-
assignmentTitle,
|
|
23707
|
-
studentId: payload.studentId,
|
|
23708
|
-
score: payload.score,
|
|
23709
|
-
feedback: payload.feedback || ""
|
|
23710
|
-
});
|
|
23711
|
-
} catch (e) {
|
|
23712
|
-
console.error("[EventBus -> Socket.IO] Error dispatching assignment graded notification:", e);
|
|
23713
|
-
}
|
|
23714
|
-
});
|
|
23715
|
-
const handleRollcallElement = (elementId) => {
|
|
23716
|
-
try {
|
|
23717
|
-
const el = kernelContainer.db.prepare("SELECT * FROM whiteboard_elements WHERE id = ?").get(elementId);
|
|
23718
|
-
if (el && el.type === "rollcall") {
|
|
23719
|
-
const elData = JSON.parse(el.data);
|
|
23720
|
-
if (elData && elData.selectedStudent && elData.status === "picked") {
|
|
23721
|
-
const studentId = elData.selectedStudent.id;
|
|
23722
|
-
const studentName = elData.selectedStudent.name;
|
|
23723
|
-
let classId = elData.classId || "";
|
|
23724
|
-
const lessonId = el.lesson_id;
|
|
23725
|
-
if (!classId && lessonId) {
|
|
23726
|
-
const sched = kernelContainer.db.prepare("SELECT class_id FROM schedules WHERE lesson_id = ? LIMIT 1").get(lessonId);
|
|
23727
|
-
if (sched) {
|
|
23728
|
-
classId = sched.class_id;
|
|
23729
|
-
}
|
|
23730
|
-
}
|
|
23731
|
-
const pickedTimeStr = elData.pickedTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
23732
|
-
const pickedTime = new Date(pickedTimeStr).getTime();
|
|
23733
|
-
const rollcallId = `rollcall-${elementId}-${pickedTime}`;
|
|
23734
|
-
const exists = kernelContainer.db.prepare("SELECT id FROM student_rollcalls WHERE id = ?").get(rollcallId);
|
|
23735
|
-
if (!exists) {
|
|
23736
|
-
kernelContainer.db.prepare(
|
|
23737
|
-
"INSERT INTO student_rollcalls (id, student_id, class_id, lesson_id, picked_time) VALUES (?, ?, ?, ?, ?)"
|
|
23738
|
-
).run(rollcallId, studentId, classId, lessonId, pickedTime);
|
|
23739
|
-
console.log(`[Rollcall] Saved rollcall for student ${studentId} (${studentName})`);
|
|
23740
|
-
io.emit("student-picked", {
|
|
23741
|
-
rollcallId,
|
|
23742
|
-
studentId,
|
|
23743
|
-
studentName,
|
|
23744
|
-
classId,
|
|
23745
|
-
lessonId,
|
|
23746
|
-
pickedTime
|
|
23747
|
-
});
|
|
23748
|
-
}
|
|
23749
|
-
}
|
|
23750
|
-
}
|
|
23751
|
-
} catch (e) {
|
|
23752
|
-
console.error("Error handling rollcall element:", e);
|
|
23753
|
-
}
|
|
23754
|
-
};
|
|
23755
|
-
kernelContainer.eventBus.subscribe("whiteboard.element_drawn", (event) => {
|
|
23756
|
-
try {
|
|
23757
|
-
const payload = event.payload;
|
|
23758
|
-
if (payload.type === "rollcall") {
|
|
23759
|
-
handleRollcallElement(payload.elementId);
|
|
23760
|
-
}
|
|
23761
|
-
if (payload.lessonId) {
|
|
23762
|
-
const syncMsg = { roomId: payload.lessonId, type: "refresh" };
|
|
23763
|
-
io.to(payload.lessonId).emit("whiteboard-sync", syncMsg);
|
|
23764
|
-
io.to("whiteboard-broadcast").emit("whiteboard-sync", syncMsg);
|
|
23765
|
-
console.log(`[EventBus -> Socket.IO] Broadcast whiteboard refresh for lesson "${payload.lessonId}" (element: "${payload.elementId}", type: "${payload.type}")`);
|
|
23766
|
-
}
|
|
23767
|
-
} catch (e) {
|
|
23768
|
-
console.error("[EventBus -> Socket.IO] Error processing whiteboard.element_drawn:", e);
|
|
23769
|
-
}
|
|
23770
|
-
});
|
|
23771
|
-
kernelContainer.eventBus.subscribe("whiteboard.element_updated", (event) => {
|
|
23772
|
-
try {
|
|
23773
|
-
const payload = event.payload;
|
|
23774
|
-
handleRollcallElement(payload.elementId);
|
|
23775
|
-
} catch (e) {
|
|
23776
|
-
console.error("[EventBus -> Socket.IO] Error processing whiteboard.element_updated for rollcall:", e);
|
|
23777
|
-
}
|
|
23778
|
-
});
|
|
23779
|
-
kernelContainer.eventBus.subscribe("whiteboard.batch_drawn", (event) => {
|
|
23780
|
-
try {
|
|
23781
|
-
const payload = event.payload;
|
|
23782
|
-
if (payload.lessonId) {
|
|
23783
|
-
io.to(payload.lessonId).emit("whiteboard-sync", {
|
|
23784
|
-
roomId: payload.lessonId,
|
|
23785
|
-
type: "refresh"
|
|
23786
|
-
});
|
|
23787
|
-
console.log(`[EventBus -> Socket.IO] Broadcast refresh after batch_draw (${payload.count} elements) for lesson "${payload.lessonId}"`);
|
|
23788
|
-
}
|
|
23789
|
-
} catch (e) {
|
|
23790
|
-
console.error("[EventBus -> Socket.IO] Error processing whiteboard.batch_drawn:", e);
|
|
23791
|
-
}
|
|
23792
|
-
});
|
|
23793
|
-
kernelContainer.eventBus.subscribe("whiteboard.element_deleted", (event) => {
|
|
23794
|
-
try {
|
|
23795
|
-
const payload = event.payload;
|
|
23796
|
-
if (payload.lessonId) {
|
|
23797
|
-
io.to(payload.lessonId).emit("whiteboard-sync", {
|
|
23798
|
-
roomId: payload.lessonId,
|
|
23799
|
-
type: "refresh"
|
|
23800
|
-
});
|
|
23801
|
-
}
|
|
23802
|
-
} catch (e) {
|
|
23803
|
-
console.error("[EventBus -> Socket.IO] Error processing whiteboard.element_deleted:", e);
|
|
23804
|
-
}
|
|
23805
|
-
});
|
|
23806
|
-
kernelContainer.eventBus.subscribe("whiteboard.cleared", (event) => {
|
|
23807
|
-
try {
|
|
23808
|
-
const payload = event.payload;
|
|
23809
|
-
if (payload.lessonId) {
|
|
23810
|
-
io.to(payload.lessonId).emit("whiteboard-sync", {
|
|
23811
|
-
roomId: payload.lessonId,
|
|
23812
|
-
type: "refresh"
|
|
23813
|
-
});
|
|
23814
|
-
}
|
|
23815
|
-
} catch (e) {
|
|
23816
|
-
console.error("[EventBus -> Socket.IO] Error processing whiteboard.cleared:", e);
|
|
23817
|
-
}
|
|
23818
|
-
});
|
|
23819
|
-
kernelContainer.eventBus.subscribe("spotlight:state_updated", (event) => {
|
|
23820
|
-
try {
|
|
23821
|
-
io.emit("spotlight:state_updated", event.payload);
|
|
23822
|
-
} catch (e) {
|
|
23823
|
-
console.error("[EventBus -> Socket.IO] Error processing spotlight:state_updated:", e);
|
|
23824
|
-
}
|
|
23825
|
-
});
|
|
23826
|
-
kernelContainer.eventBus.subscribe("spotlight.state_updated", (event) => {
|
|
23827
|
-
try {
|
|
23828
|
-
io.emit("spotlight:state_updated", event.payload);
|
|
23829
|
-
} catch (e) {
|
|
23830
|
-
console.error("[EventBus -> Socket.IO] Error processing spotlight.state_updated:", e);
|
|
23831
|
-
}
|
|
23832
|
-
});
|
|
23833
|
-
const onlineStudents = /* @__PURE__ */ new Map();
|
|
23834
|
-
const activeStudentLessons = /* @__PURE__ */ new Map();
|
|
23835
|
-
const broadcastPresence = () => {
|
|
23836
|
-
io.emit("presence-update", {
|
|
23837
|
-
onlineStudentIds: Array.from(onlineStudents.keys()),
|
|
23838
|
-
activeStudentLessons: Object.fromEntries(activeStudentLessons.entries())
|
|
23839
|
-
});
|
|
23840
|
-
};
|
|
23841
|
-
io.on("connection", (socket) => {
|
|
23842
|
-
let registeredStudentId = null;
|
|
23843
|
-
socket.on("register-student", (data) => {
|
|
23844
|
-
registeredStudentId = data.studentId;
|
|
23845
|
-
onlineStudents.set(data.studentId, { socketId: socket.id, name: data.name });
|
|
23846
|
-
console.log(`[Presence] Student online: ${data.name} (${data.studentId})`);
|
|
23847
|
-
broadcastPresence();
|
|
23848
|
-
});
|
|
23849
|
-
socket.on("enter-lesson", (data) => {
|
|
23850
|
-
activeStudentLessons.set(data.studentId, data.lessonId);
|
|
23851
|
-
socket.join(data.lessonId);
|
|
23852
|
-
console.log(`[Presence] Student ${data.studentId} entered lesson ${data.lessonId}`);
|
|
23853
|
-
broadcastPresence();
|
|
23854
|
-
const activeSeg = lessonActiveSegments.get(data.lessonId);
|
|
23855
|
-
if (activeSeg) {
|
|
23856
|
-
socket.emit("student-active-segment-changed", {
|
|
23857
|
-
lessonId: data.lessonId,
|
|
23858
|
-
activeSegmentId: activeSeg
|
|
23859
|
-
});
|
|
23860
|
-
}
|
|
23861
|
-
});
|
|
23862
|
-
socket.on("leave-lesson", (data) => {
|
|
23863
|
-
const oldRoom = activeStudentLessons.get(data.studentId);
|
|
23864
|
-
if (oldRoom) {
|
|
23865
|
-
socket.leave(oldRoom);
|
|
23866
|
-
}
|
|
23867
|
-
activeStudentLessons.delete(data.studentId);
|
|
23868
|
-
console.log(`[Presence] Student ${data.studentId} left lesson`);
|
|
23869
|
-
broadcastPresence();
|
|
23870
|
-
});
|
|
23871
|
-
socket.on("join-room", (roomId) => {
|
|
23872
|
-
socket.join(roomId);
|
|
23873
|
-
});
|
|
23874
|
-
socket.on("whiteboard-update", (data) => {
|
|
23875
|
-
socket.to(data.roomId).emit("whiteboard-sync", data);
|
|
23876
|
-
});
|
|
23877
|
-
socket.on("whiteboard-event", (data) => {
|
|
23878
|
-
kernelContainer.eventBus.publish({
|
|
23879
|
-
id: data.id,
|
|
23880
|
-
type: data.type,
|
|
23881
|
-
source: "whiteboard",
|
|
23882
|
-
payload: data.payload,
|
|
23883
|
-
timestamp: data.timestamp,
|
|
23884
|
-
correlationId: data.payload.lessonId
|
|
23885
|
-
});
|
|
23886
|
-
const lessonId = data.payload.lessonId;
|
|
23887
|
-
if (lessonId) {
|
|
23888
|
-
const roomName = lessonId.startsWith("assignment-") ? lessonId : `lesson-${lessonId}`;
|
|
23889
|
-
socket.to(data.payload.lessonId).emit("whiteboard-sync", {
|
|
23890
|
-
type: "refresh",
|
|
23891
|
-
sourceEvent: data.type
|
|
23892
|
-
});
|
|
23893
|
-
}
|
|
23894
|
-
});
|
|
23895
|
-
socket.on("teacher-broadcast-segment", (data) => {
|
|
23896
|
-
lessonActiveSegments.set(data.lessonId, data.activeSegmentId);
|
|
23897
|
-
io.to(data.lessonId).emit("student-active-segment-changed", data);
|
|
23898
|
-
});
|
|
23899
|
-
socket.on("teacher-ping-student", (data) => {
|
|
23900
|
-
console.log(`[Ping] Teacher pinged student ${data.studentId} for lesson ${data.lessonId}`);
|
|
23901
|
-
const studentOnlineInfo = onlineStudents.get(data.studentId);
|
|
23902
|
-
if (studentOnlineInfo) {
|
|
23903
|
-
io.to(studentOnlineInfo.socketId).emit("student-pinged", {
|
|
23904
|
-
lessonId: data.lessonId,
|
|
23905
|
-
message: data.message
|
|
23906
|
-
});
|
|
23907
|
-
}
|
|
23908
|
-
});
|
|
23909
|
-
socket.on("disconnect", () => {
|
|
23910
|
-
if (registeredStudentId) {
|
|
23911
|
-
onlineStudents.delete(registeredStudentId);
|
|
23912
|
-
activeStudentLessons.delete(registeredStudentId);
|
|
23913
|
-
console.log(`[Presence] Student offline: ${registeredStudentId}`);
|
|
23914
|
-
broadcastPresence();
|
|
23915
|
-
}
|
|
23916
|
-
});
|
|
23917
|
-
socket.emit("presence-update", {
|
|
23918
|
-
onlineStudentIds: Array.from(onlineStudents.keys()),
|
|
23919
|
-
activeStudentLessons: Object.fromEntries(activeStudentLessons.entries())
|
|
23920
|
-
});
|
|
23921
|
-
});
|
|
24034
|
+
setupRealtimeBridge({ eventBus: kernelContainer.eventBus, io, db: kernelContainer.db });
|
|
24035
|
+
setupPresence({ io, eventBus: kernelContainer.eventBus });
|
|
23922
24036
|
if (process.env.NODE_ENV !== "production") {
|
|
23923
24037
|
const vite = await (0, import_vite.createServer)({
|
|
23924
24038
|
server: {
|