openlearn-next 0.1.16 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{courseware-fTX0oFYe.js → courseware-BfnpYR-P.js} +1 -1
- package/dist/assets/index-CaJDJGky.css +1 -0
- package/dist/assets/index-zi7zNIte.js +682 -0
- package/dist/assets/{index.es-BLnapfB6.js → index.es-S9HBYdjC.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-BpCj1CQU.js} +27 -23
- package/dist/index.html +2 -2
- package/dist/server.cjs +1698 -1662
- 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,713 +17110,1344 @@ 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
|
-
|
|
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}`);
|
|
17719
17693
|
}
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
const token = getCookieToken(req);
|
|
17725
|
-
if (!token) return "user-frontend";
|
|
17726
|
-
try {
|
|
17727
|
-
const session = getValidSession(token);
|
|
17728
|
-
if (!session) return "user-frontend";
|
|
17729
|
-
let role = session.subRole || session.role;
|
|
17730
|
-
if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
|
|
17731
|
-
role = "administrator";
|
|
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");
|
|
17732
17698
|
}
|
|
17733
|
-
|
|
17734
|
-
|
|
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
|
|
17705
|
+
});
|
|
17706
|
+
if (toolCalls.length === 0) {
|
|
17707
|
+
break;
|
|
17735
17708
|
}
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
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 = {};
|
|
17718
|
+
}
|
|
17719
|
+
}
|
|
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;
|
|
17727
|
+
}
|
|
17728
|
+
}
|
|
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
|
+
});
|
|
17737
|
+
}
|
|
17738
|
+
loopCount++;
|
|
17739
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;
|
|
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.";
|
|
17750
17742
|
}
|
|
17751
|
-
|
|
17743
|
+
return {
|
|
17744
|
+
agentText: finalResponseText,
|
|
17745
|
+
toolResults: allExecutedTools
|
|
17746
|
+
};
|
|
17747
|
+
};
|
|
17752
17748
|
|
|
17753
|
-
//
|
|
17754
|
-
var
|
|
17755
|
-
|
|
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);
|
|
17754
|
+
}
|
|
17755
|
+
if (context.environment) {
|
|
17756
|
+
builder.withEnvironment(context.environment);
|
|
17757
|
+
}
|
|
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);
|
|
17768
|
+
}
|
|
17769
|
+
}
|
|
17770
|
+
if (context.kernelContainer) {
|
|
17771
|
+
builder.addService("kernelContainer", context.kernelContainer);
|
|
17772
|
+
}
|
|
17773
|
+
if (context.expressApp) {
|
|
17774
|
+
builder.addService("expressApp", context.expressApp);
|
|
17775
|
+
}
|
|
17776
|
+
if (context.httpServer) {
|
|
17777
|
+
builder.addService("httpServer", context.httpServer);
|
|
17778
|
+
}
|
|
17779
|
+
}
|
|
17780
|
+
static registerExistingBootstrapStages(_builder) {
|
|
17781
|
+
}
|
|
17782
|
+
};
|
|
17756
17783
|
|
|
17757
|
-
//
|
|
17758
|
-
var
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
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: () => {
|
|
17834
|
+
}
|
|
17765
17835
|
});
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17836
|
+
if (pipelineResult.status === "Failed") {
|
|
17837
|
+
throw pipelineResult.error || new Error(`ServerBootstrapAdapter pipeline failed at stage: ${pipelineResult.failedStage}`);
|
|
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);
|
|
17774
17845
|
}
|
|
17846
|
+
};
|
|
17775
17847
|
|
|
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);
|
|
17800
|
-
}
|
|
17801
|
-
throw err;
|
|
17802
|
-
}
|
|
17803
|
-
};
|
|
17848
|
+
// packages/activity-ecosystem/index.ts
|
|
17849
|
+
init_token();
|
|
17804
17850
|
|
|
17805
|
-
|
|
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
|
+
}
|
|
17862
|
+
|
|
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
|
+
// Proxy postMessage calls to enrich them with attempt_id/uuid and normalize targetOrigin
|
|
18422
|
+
try {
|
|
18423
|
+
const originalPostMessage = window.postMessage;
|
|
18424
|
+
window.postMessage = function(message, targetOrigin, transfer) {
|
|
18425
|
+
try {
|
|
18426
|
+
if (message && typeof message === 'object') {
|
|
18427
|
+
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18428
|
+
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18429
|
+
}
|
|
18430
|
+
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18431
|
+
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18432
|
+
}
|
|
18433
|
+
}
|
|
18434
|
+
} catch (e) {}
|
|
18435
|
+
|
|
18436
|
+
let origin = targetOrigin;
|
|
18437
|
+
if (origin === 'null') {
|
|
18438
|
+
origin = '*';
|
|
18439
|
+
}
|
|
18440
|
+
try {
|
|
18441
|
+
return originalPostMessage.call(this, message, origin, transfer);
|
|
18442
|
+
} catch (err) {
|
|
18443
|
+
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18444
|
+
return originalPostMessage.call(this, message, '*', transfer);
|
|
18445
|
+
}
|
|
18446
|
+
throw err;
|
|
18447
|
+
}
|
|
18448
|
+
};
|
|
18449
|
+
|
|
18450
|
+
if (window.parent && window.parent !== window) {
|
|
17806
18451
|
const parentPostMessage = window.parent.postMessage;
|
|
17807
18452
|
try {
|
|
17808
18453
|
window.parent.postMessage = function(message, targetOrigin, transfer) {
|
|
@@ -18264,7 +18909,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
18264
18909
|
const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
|
|
18265
18910
|
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
18911
|
if (!attempt) {
|
|
18267
|
-
const attemptId = "att_" +
|
|
18912
|
+
const attemptId = "att_" + import_crypto6.default.randomBytes(8).toString("hex");
|
|
18268
18913
|
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
18914
|
attempt = { id: attemptId };
|
|
18270
18915
|
}
|
|
@@ -18277,7 +18922,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
18277
18922
|
} else if (session.role === "teacher" || session.role === "administrator") {
|
|
18278
18923
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
|
|
18279
18924
|
if (!attempt) {
|
|
18280
|
-
const attemptId = "att_teacher_" +
|
|
18925
|
+
const attemptId = "att_teacher_" + import_crypto6.default.randomBytes(8).toString("hex");
|
|
18281
18926
|
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
18927
|
attempt = { id: attemptId };
|
|
18283
18928
|
}
|
|
@@ -18293,7 +18938,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
18293
18938
|
if (studentInfo.attempt_id === "guest-attempt") {
|
|
18294
18939
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
|
|
18295
18940
|
if (!attempt) {
|
|
18296
|
-
const attemptId = "att_guest_" +
|
|
18941
|
+
const attemptId = "att_guest_" + import_crypto6.default.randomBytes(8).toString("hex");
|
|
18297
18942
|
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
18943
|
attempt = { id: attemptId };
|
|
18299
18944
|
}
|
|
@@ -18381,7 +19026,7 @@ function registerOsRoutes(ctx) {
|
|
|
18381
19026
|
if (!import_fs8.default.existsSync(uploadsDir)) {
|
|
18382
19027
|
import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
|
|
18383
19028
|
}
|
|
18384
|
-
const uniqueName = `${Date.now()}-${
|
|
19029
|
+
const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
|
|
18385
19030
|
const filePath = import_path8.default.join(uploadsDir, uniqueName);
|
|
18386
19031
|
import_fs8.default.writeFileSync(filePath, fileBuffer);
|
|
18387
19032
|
let slideCount = 1;
|
|
@@ -18591,8 +19236,8 @@ function registerOsRoutes(ctx) {
|
|
|
18591
19236
|
const result = provider ? await runOpenAIAgentChat2(provider, { message, lang, currentLessonId, attachments, callerRole, history }) : await runGeminiAgentChat2({ message, lang, currentLessonId, attachments, callerRole, history });
|
|
18592
19237
|
if (result && typeof result.agentText === "string") {
|
|
18593
19238
|
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_" +
|
|
19239
|
+
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);
|
|
19240
|
+
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
19241
|
}
|
|
18597
19242
|
res.json({
|
|
18598
19243
|
success: true,
|
|
@@ -18934,7 +19579,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
18934
19579
|
// server/routes/courseware.ts
|
|
18935
19580
|
var import_path9 = __toESM(require("path"), 1);
|
|
18936
19581
|
var import_fs9 = __toESM(require("fs"), 1);
|
|
18937
|
-
var
|
|
19582
|
+
var import_crypto9 = __toESM(require("crypto"), 1);
|
|
18938
19583
|
function registerCoursewareRoutes(ctx) {
|
|
18939
19584
|
const {
|
|
18940
19585
|
app,
|
|
@@ -19079,7 +19724,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19079
19724
|
try {
|
|
19080
19725
|
const { attemptId } = req.params;
|
|
19081
19726
|
const { eventType, payload } = req.body;
|
|
19082
|
-
const rawId = "raw_" +
|
|
19727
|
+
const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
|
|
19083
19728
|
kernelContainer.db.prepare(
|
|
19084
19729
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
19085
19730
|
).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
|
|
@@ -19107,7 +19752,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19107
19752
|
kernelContainer.db.prepare(
|
|
19108
19753
|
"INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
|
|
19109
19754
|
).run(
|
|
19110
|
-
"res_" +
|
|
19755
|
+
"res_" + import_crypto9.default.randomBytes(8).toString("hex"),
|
|
19111
19756
|
attemptId,
|
|
19112
19757
|
parsedScore,
|
|
19113
19758
|
comment || null,
|
|
@@ -19258,7 +19903,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19258
19903
|
).get(classId, lessonId, assignmentTitle);
|
|
19259
19904
|
let assignmentId = assignment?.id;
|
|
19260
19905
|
if (!assignmentId) {
|
|
19261
|
-
assignmentId = "ast-cw-" +
|
|
19906
|
+
assignmentId = "ast-cw-" + import_crypto9.default.randomBytes(8).toString("hex");
|
|
19262
19907
|
kernelContainer.db.prepare(
|
|
19263
19908
|
"INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
19264
19909
|
).run(
|
|
@@ -19415,7 +20060,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
19415
20060
|
}
|
|
19416
20061
|
|
|
19417
20062
|
// server/routes/lessons.ts
|
|
19418
|
-
var
|
|
20063
|
+
var import_genai3 = require("@google/genai");
|
|
19419
20064
|
function registerLessonsRoutes(ctx) {
|
|
19420
20065
|
const {
|
|
19421
20066
|
app,
|
|
@@ -19796,7 +20441,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
19796
20441
|
app.post("/api/lessons/:id/ai-tutor", async (req, res) => {
|
|
19797
20442
|
try {
|
|
19798
20443
|
const { elements } = req.body;
|
|
19799
|
-
const ai = new
|
|
20444
|
+
const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
19800
20445
|
const elementsSummary = elements.map((e, i) => `Element ${i + 1}: type=${e.type}, content=${JSON.stringify(e.data)}`).join("\n");
|
|
19801
20446
|
const prompt = `You are a real-time AI Tutor monitoring a student's interactive whiteboard.
|
|
19802
20447
|
The student has pressed the "Ask AI" button for help.
|
|
@@ -20226,7 +20871,7 @@ function registerAdminRoutes(ctx) {
|
|
|
20226
20871
|
// server/routes/roster.ts
|
|
20227
20872
|
var import_path12 = __toESM(require("path"), 1);
|
|
20228
20873
|
var import_fs11 = __toESM(require("fs"), 1);
|
|
20229
|
-
var
|
|
20874
|
+
var import_crypto10 = __toESM(require("crypto"), 1);
|
|
20230
20875
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
20231
20876
|
function registerRosterRoutes(ctx) {
|
|
20232
20877
|
const {
|
|
@@ -20486,7 +21131,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20486
21131
|
if (storedPwd.startsWith("$2")) {
|
|
20487
21132
|
matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
|
|
20488
21133
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
20489
|
-
matches =
|
|
21134
|
+
matches = import_crypto10.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
|
|
20490
21135
|
} else {
|
|
20491
21136
|
matches = storedPwd === oldPassword;
|
|
20492
21137
|
}
|
|
@@ -20558,7 +21203,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20558
21203
|
}
|
|
20559
21204
|
const avatarDir = import_path12.default.join(process.cwd(), "uploads", "avatars");
|
|
20560
21205
|
import_fs11.default.mkdirSync(avatarDir, { recursive: true });
|
|
20561
|
-
const uniqueName = `${Date.now()}-${
|
|
21206
|
+
const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
|
|
20562
21207
|
const filePath = import_path12.default.join(avatarDir, uniqueName);
|
|
20563
21208
|
import_fs11.default.writeFileSync(filePath, fileBuffer);
|
|
20564
21209
|
const avatarUrl = `/uploads/avatars/${uniqueName}`;
|
|
@@ -20677,7 +21322,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20677
21322
|
if (storedPwd.startsWith("$2")) {
|
|
20678
21323
|
matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
|
|
20679
21324
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
20680
|
-
const sha256Hash =
|
|
21325
|
+
const sha256Hash = import_crypto10.default.createHash("sha256").update(providedPassword).digest("hex");
|
|
20681
21326
|
if (sha256Hash === storedPwd) {
|
|
20682
21327
|
matchesOwnPassword = true;
|
|
20683
21328
|
kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
|
|
@@ -20716,7 +21361,7 @@ function registerRosterRoutes(ctx) {
|
|
|
20716
21361
|
};
|
|
20717
21362
|
}
|
|
20718
21363
|
if (sessionData) {
|
|
20719
|
-
const sessionToken = "token_" +
|
|
21364
|
+
const sessionToken = "token_" + import_crypto10.default.randomBytes(16).toString("hex");
|
|
20720
21365
|
const now = Date.now();
|
|
20721
21366
|
const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
|
|
20722
21367
|
kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
|
|
@@ -21295,7 +21940,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21295
21940
|
}
|
|
21296
21941
|
|
|
21297
21942
|
// server/routes/assignments.ts
|
|
21298
|
-
var
|
|
21943
|
+
var import_genai4 = require("@google/genai");
|
|
21299
21944
|
function registerAssignmentsRoutes(ctx) {
|
|
21300
21945
|
const {
|
|
21301
21946
|
app,
|
|
@@ -21323,7 +21968,7 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
21323
21968
|
app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
|
|
21324
21969
|
try {
|
|
21325
21970
|
const { topic, lessonId } = req.body;
|
|
21326
|
-
const ai = new
|
|
21971
|
+
const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
21327
21972
|
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
21973
|
const response = await ai.models.generateContent({ model: "gemini-3.5-flash", contents: prompt });
|
|
21329
21974
|
const text = response.text || "{}";
|
|
@@ -21355,7 +22000,7 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
21355
22000
|
if (!lesson) {
|
|
21356
22001
|
return res.status(404).json({ error: "Lesson not found" });
|
|
21357
22002
|
}
|
|
21358
|
-
const ai = new
|
|
22003
|
+
const ai = new import_genai4.GoogleGenAI({
|
|
21359
22004
|
apiKey: process.env.GEMINI_API_KEY,
|
|
21360
22005
|
httpOptions: {
|
|
21361
22006
|
headers: {
|
|
@@ -21380,26 +22025,26 @@ Generate the response in the specified JSON schema.`;
|
|
|
21380
22025
|
config: {
|
|
21381
22026
|
responseMimeType: "application/json",
|
|
21382
22027
|
responseSchema: {
|
|
21383
|
-
type:
|
|
22028
|
+
type: import_genai4.Type.OBJECT,
|
|
21384
22029
|
properties: {
|
|
21385
22030
|
learningObjectives: {
|
|
21386
|
-
type:
|
|
21387
|
-
items: { type:
|
|
22031
|
+
type: import_genai4.Type.ARRAY,
|
|
22032
|
+
items: { type: import_genai4.Type.STRING },
|
|
21388
22033
|
description: "List of identified key learning objectives for the lesson"
|
|
21389
22034
|
},
|
|
21390
22035
|
questions: {
|
|
21391
|
-
type:
|
|
22036
|
+
type: import_genai4.Type.ARRAY,
|
|
21392
22037
|
items: {
|
|
21393
|
-
type:
|
|
22038
|
+
type: import_genai4.Type.OBJECT,
|
|
21394
22039
|
properties: {
|
|
21395
|
-
objective: { type:
|
|
21396
|
-
question: { type:
|
|
22040
|
+
objective: { type: import_genai4.Type.STRING, description: "The specific learning objective tested by this question" },
|
|
22041
|
+
question: { type: import_genai4.Type.STRING, description: "The multiple-choice question text" },
|
|
21397
22042
|
options: {
|
|
21398
|
-
type:
|
|
21399
|
-
items: { type:
|
|
22043
|
+
type: import_genai4.Type.ARRAY,
|
|
22044
|
+
items: { type: import_genai4.Type.STRING },
|
|
21400
22045
|
description: "Exactly 4 options, including letter prefix like 'A) ...', 'B) ...'"
|
|
21401
22046
|
},
|
|
21402
|
-
correctAnswer: { type:
|
|
22047
|
+
correctAnswer: { type: import_genai4.Type.STRING, description: "The correct option (must exactly match one of the string options in the options array)" }
|
|
21403
22048
|
},
|
|
21404
22049
|
required: ["objective", "question", "options", "correctAnswer"]
|
|
21405
22050
|
}
|
|
@@ -21471,7 +22116,7 @@ Generate the response in the specified JSON schema.`;
|
|
|
21471
22116
|
const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
|
|
21472
22117
|
const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
|
|
21473
22118
|
if (!asb || !ast) throw new Error("Submission or assignment not found");
|
|
21474
|
-
const ai = new
|
|
22119
|
+
const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
21475
22120
|
let grade = { score: 0, feedback: "" };
|
|
21476
22121
|
let isMcqQuiz = false;
|
|
21477
22122
|
let autoScore = null;
|
|
@@ -21554,7 +22199,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
21554
22199
|
}
|
|
21555
22200
|
|
|
21556
22201
|
// server/routes/schedules.ts
|
|
21557
|
-
var
|
|
22202
|
+
var import_genai5 = require("@google/genai");
|
|
21558
22203
|
function registerSchedulesRoutes(ctx) {
|
|
21559
22204
|
const {
|
|
21560
22205
|
app,
|
|
@@ -21841,7 +22486,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
21841
22486
|
console.warn(`[OCR Error] GEMINI_API_KEY is not configured`);
|
|
21842
22487
|
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
22488
|
}
|
|
21844
|
-
const ai = new
|
|
22489
|
+
const ai = new import_genai5.GoogleGenAI({ apiKey: geminiKey });
|
|
21845
22490
|
const response = await ai.models.generateContent({
|
|
21846
22491
|
model: "gemini-2.5-flash",
|
|
21847
22492
|
contents: [{
|
|
@@ -21886,7 +22531,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
21886
22531
|
|
|
21887
22532
|
// server/routes/grading.ts
|
|
21888
22533
|
init_interfaces();
|
|
21889
|
-
var
|
|
22534
|
+
var import_genai6 = require("@google/genai");
|
|
21890
22535
|
function registerGradingRoutes(ctx) {
|
|
21891
22536
|
const {
|
|
21892
22537
|
app,
|
|
@@ -22454,7 +23099,7 @@ ${examsText}
|
|
|
22454
23099
|
if (!geminiKey) {
|
|
22455
23100
|
return res.status(500).json({ error: "AI provider is not configured and GEMINI_API_KEY is missing." });
|
|
22456
23101
|
}
|
|
22457
|
-
const ai = new
|
|
23102
|
+
const ai = new import_genai6.GoogleGenAI({ apiKey: geminiKey });
|
|
22458
23103
|
const response = await ai.models.generateContent({
|
|
22459
23104
|
model: "gemini-2.5-flash",
|
|
22460
23105
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
@@ -22809,798 +23454,411 @@ function registerPluginsRoutes(ctx) {
|
|
|
22809
23454
|
const zipPath = import_path13.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
|
|
22810
23455
|
if (import_fs12.default.existsSync(zipPath)) {
|
|
22811
23456
|
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 });
|
|
23457
|
+
} else {
|
|
23458
|
+
return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
|
|
23459
|
+
}
|
|
23460
|
+
}
|
|
23461
|
+
const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23462
|
+
targetPluginId,
|
|
23463
|
+
allowDowngrade: false
|
|
23464
|
+
});
|
|
23465
|
+
res.json({
|
|
23466
|
+
success: true,
|
|
23467
|
+
updated: true,
|
|
23468
|
+
pluginId: result.pluginId,
|
|
23469
|
+
manifest: result.manifest,
|
|
23470
|
+
oldVersion: result.oldVersion,
|
|
23471
|
+
newVersion: result.newVersion || "1.2.0",
|
|
23472
|
+
wasActive: result.wasActive
|
|
23473
|
+
});
|
|
23067
23474
|
} catch (err) {
|
|
23068
|
-
console.error(
|
|
23475
|
+
console.error(err);
|
|
23069
23476
|
res.status(500).json({ success: false, error: err.message });
|
|
23070
23477
|
}
|
|
23071
23478
|
});
|
|
23072
|
-
app.get("/api/
|
|
23479
|
+
app.get("/api/plugins/:id(*)/contributions", (req, res) => {
|
|
23073
23480
|
try {
|
|
23074
|
-
const
|
|
23075
|
-
const
|
|
23076
|
-
|
|
23077
|
-
api_key: maskApiKey(decryptApiKey(p.api_key || ""))
|
|
23078
|
-
}));
|
|
23079
|
-
res.json(masked);
|
|
23481
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23482
|
+
const summary = kernelContainer.pluginHost.listContributions(rawId);
|
|
23483
|
+
res.json({ success: true, result: summary });
|
|
23080
23484
|
} catch (e) {
|
|
23081
|
-
res.status(500).json({ error: e.message });
|
|
23485
|
+
res.status(500).json({ success: false, error: e.message });
|
|
23082
23486
|
}
|
|
23083
23487
|
});
|
|
23084
|
-
app.
|
|
23488
|
+
app.get("/api/plugins/:id(*)/config", (req, res) => {
|
|
23085
23489
|
try {
|
|
23086
|
-
const
|
|
23087
|
-
|
|
23088
|
-
|
|
23490
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23491
|
+
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
23492
|
+
const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
|
|
23493
|
+
if (!row) {
|
|
23494
|
+
return res.status(404).json({ success: false, error: "Plugin not found" });
|
|
23089
23495
|
}
|
|
23090
|
-
const
|
|
23091
|
-
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
|
|
23496
|
+
const manifest = JSON.parse(row.manifest);
|
|
23497
|
+
res.json({
|
|
23498
|
+
success: true,
|
|
23499
|
+
result: {
|
|
23500
|
+
schema: manifest.configuration?.properties ?? {},
|
|
23501
|
+
values: kernelContainer.pluginHost.getPluginConfig(pluginId, manifest)
|
|
23502
|
+
}
|
|
23503
|
+
});
|
|
23095
23504
|
} catch (e) {
|
|
23096
|
-
res.status(500).json({ error: e.message });
|
|
23505
|
+
res.status(500).json({ success: false, error: e.message });
|
|
23097
23506
|
}
|
|
23098
23507
|
});
|
|
23099
|
-
app.
|
|
23508
|
+
app.post("/api/plugins/:id(*)/config", (req, res) => {
|
|
23100
23509
|
try {
|
|
23101
|
-
const
|
|
23102
|
-
|
|
23103
|
-
|
|
23510
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23511
|
+
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
23512
|
+
const updates = req.body;
|
|
23513
|
+
if (!updates || typeof updates !== "object") {
|
|
23514
|
+
return res.status(400).json({ success: false, error: "Body must be an object of key-value pairs" });
|
|
23104
23515
|
}
|
|
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 || "";
|
|
23516
|
+
const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
|
|
23517
|
+
if (!row) {
|
|
23518
|
+
return res.status(404).json({ success: false, error: "Plugin not found" });
|
|
23112
23519
|
}
|
|
23113
|
-
|
|
23520
|
+
const manifest = JSON.parse(row.manifest);
|
|
23521
|
+
kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
|
|
23114
23522
|
res.json({ success: true });
|
|
23115
23523
|
} catch (e) {
|
|
23116
|
-
res.status(500).json({ error: e.message });
|
|
23524
|
+
res.status(500).json({ success: false, error: e.message });
|
|
23117
23525
|
}
|
|
23118
23526
|
});
|
|
23119
|
-
app.
|
|
23527
|
+
app.post("/api/plugins/:id(*)/toggle", async (req, res) => {
|
|
23120
23528
|
try {
|
|
23121
|
-
|
|
23122
|
-
|
|
23123
|
-
|
|
23124
|
-
|
|
23529
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23530
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23531
|
+
"plugin.toggle",
|
|
23532
|
+
{ pluginId: rawId },
|
|
23533
|
+
getActorId(req)
|
|
23534
|
+
);
|
|
23535
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23536
|
+
res.json(result);
|
|
23537
|
+
} catch (err) {
|
|
23538
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23125
23539
|
}
|
|
23126
23540
|
});
|
|
23127
|
-
app.
|
|
23541
|
+
app.delete("/api/plugins/:id(*)", async (req, res) => {
|
|
23128
23542
|
try {
|
|
23129
|
-
const
|
|
23130
|
-
|
|
23131
|
-
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
res.
|
|
23543
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23544
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23545
|
+
"plugin.uninstall",
|
|
23546
|
+
{ pluginId: rawId },
|
|
23547
|
+
getActorId(req)
|
|
23548
|
+
);
|
|
23549
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23550
|
+
res.json(result);
|
|
23551
|
+
} catch (err) {
|
|
23552
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23137
23553
|
}
|
|
23138
23554
|
});
|
|
23139
|
-
app.
|
|
23555
|
+
app.get("/api/plugins/:id(*)", async (req, res) => {
|
|
23140
23556
|
try {
|
|
23141
|
-
const
|
|
23142
|
-
kernelContainer.
|
|
23143
|
-
|
|
23144
|
-
|
|
23145
|
-
|
|
23146
|
-
|
|
23147
|
-
|
|
23148
|
-
|
|
23149
|
-
|
|
23150
|
-
|
|
23151
|
-
res.status(500).json({ error: e.message });
|
|
23557
|
+
const rawId = decodeURIComponent(req.params.id);
|
|
23558
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23559
|
+
"plugin.info",
|
|
23560
|
+
{ pluginId: rawId },
|
|
23561
|
+
getActorId(req)
|
|
23562
|
+
);
|
|
23563
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23564
|
+
res.json(result);
|
|
23565
|
+
} catch (err) {
|
|
23566
|
+
res.status(404).json({ success: false, error: err.message });
|
|
23152
23567
|
}
|
|
23153
23568
|
});
|
|
23154
|
-
app.post("/api/
|
|
23569
|
+
app.post("/api/plugins", async (req, res) => {
|
|
23155
23570
|
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)])
|
|
23571
|
+
const { sourceCode } = req.body;
|
|
23572
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23573
|
+
"plugin.install",
|
|
23574
|
+
{ sourceCode },
|
|
23575
|
+
getActorId(req)
|
|
23265
23576
|
);
|
|
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)
|
|
23577
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23578
|
+
res.json(result);
|
|
23579
|
+
} catch (err) {
|
|
23580
|
+
console.error(err);
|
|
23581
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23284
23582
|
}
|
|
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
|
-
);
|
|
23583
|
+
});
|
|
23584
|
+
app.post("/api/plugins/upload-zip", async (req, res) => {
|
|
23300
23585
|
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
|
-
}
|
|
23586
|
+
const { base64Data, filename, executionMode } = req.body;
|
|
23587
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
23588
|
+
"plugin.install_zip",
|
|
23589
|
+
{ base64Data, filename, executionMode },
|
|
23590
|
+
getActorId(req)
|
|
23591
|
+
);
|
|
23592
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23593
|
+
res.json(result);
|
|
23330
23594
|
} catch (err) {
|
|
23331
|
-
|
|
23332
|
-
|
|
23595
|
+
console.error(err);
|
|
23596
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23333
23597
|
}
|
|
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
|
|
23598
|
+
});
|
|
23599
|
+
app.post("/api/plugins/upload-zip-raw", import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
|
|
23600
|
+
try {
|
|
23601
|
+
const zipBuffer = req.body;
|
|
23602
|
+
const filename = req.headers["x-filename"] ? decodeURIComponent(req.headers["x-filename"]) : "plugin.zip";
|
|
23603
|
+
const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
|
|
23604
|
+
const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
|
|
23605
|
+
const modeHeader = String(req.headers["x-install-mode"] || "install").toLowerCase();
|
|
23606
|
+
const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
|
|
23607
|
+
const targetPluginId = req.headers["x-target-plugin-id"] ? decodeURIComponent(String(req.headers["x-target-plugin-id"])) : void 0;
|
|
23608
|
+
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
23609
|
+
return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
|
|
23376
23610
|
}
|
|
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
|
-
}
|
|
23611
|
+
if (modeHeader === "update") {
|
|
23612
|
+
const result2 = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23613
|
+
targetPluginId,
|
|
23614
|
+
executionMode,
|
|
23615
|
+
allowDowngrade
|
|
23616
|
+
});
|
|
23617
|
+
return res.json({
|
|
23618
|
+
success: true,
|
|
23619
|
+
updated: true,
|
|
23620
|
+
pluginId: result2.pluginId,
|
|
23621
|
+
manifest: result2.manifest,
|
|
23622
|
+
oldVersion: result2.oldVersion,
|
|
23623
|
+
newVersion: result2.newVersion,
|
|
23624
|
+
wasActive: result2.wasActive,
|
|
23625
|
+
filename
|
|
23410
23626
|
});
|
|
23411
23627
|
}
|
|
23628
|
+
const result = await kernelContainer.pluginDistributionManager.installFromZip(zipBuffer, executionMode);
|
|
23629
|
+
res.json({
|
|
23630
|
+
success: true,
|
|
23631
|
+
updated: false,
|
|
23632
|
+
pluginId: result.pluginId,
|
|
23633
|
+
manifest: result.manifest,
|
|
23634
|
+
filename
|
|
23635
|
+
});
|
|
23636
|
+
} catch (err) {
|
|
23637
|
+
console.error(err);
|
|
23638
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23412
23639
|
}
|
|
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 = {};
|
|
23640
|
+
});
|
|
23641
|
+
app.post(
|
|
23642
|
+
"/api/plugins/:id(*)/update-zip-raw",
|
|
23643
|
+
import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }),
|
|
23644
|
+
async (req, res) => {
|
|
23645
|
+
try {
|
|
23646
|
+
const targetPluginId = decodeURIComponent(req.params.id);
|
|
23647
|
+
const zipBuffer = req.body;
|
|
23648
|
+
const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
|
|
23649
|
+
const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
|
|
23650
|
+
const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
|
|
23651
|
+
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
23652
|
+
return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
|
|
23489
23653
|
}
|
|
23654
|
+
const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
|
|
23655
|
+
targetPluginId,
|
|
23656
|
+
executionMode,
|
|
23657
|
+
allowDowngrade
|
|
23658
|
+
});
|
|
23659
|
+
res.json({
|
|
23660
|
+
success: true,
|
|
23661
|
+
updated: true,
|
|
23662
|
+
pluginId: result.pluginId,
|
|
23663
|
+
manifest: result.manifest,
|
|
23664
|
+
oldVersion: result.oldVersion,
|
|
23665
|
+
newVersion: result.newVersion,
|
|
23666
|
+
wasActive: result.wasActive
|
|
23667
|
+
});
|
|
23668
|
+
} catch (err) {
|
|
23669
|
+
console.error(err);
|
|
23670
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23490
23671
|
}
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
|
|
23496
|
-
|
|
23497
|
-
|
|
23672
|
+
}
|
|
23673
|
+
);
|
|
23674
|
+
app.post("/api/plugins/execute-command", async (req, res) => {
|
|
23675
|
+
try {
|
|
23676
|
+
const { type, payload } = req.body;
|
|
23677
|
+
if (!type) {
|
|
23678
|
+
return res.status(400).json({ success: false, error: "Missing command type" });
|
|
23679
|
+
}
|
|
23680
|
+
let resolvedType = type;
|
|
23681
|
+
const bus = kernelContainer.commandBus;
|
|
23682
|
+
const handlersMap = bus.handlers;
|
|
23683
|
+
const legacyMap = bus.legacyHandlers;
|
|
23684
|
+
if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
|
|
23685
|
+
for (const map of [handlersMap, legacyMap]) {
|
|
23686
|
+
if (!map) continue;
|
|
23687
|
+
for (const [key] of map) {
|
|
23688
|
+
if (key.endsWith(":" + resolvedType) || key.endsWith("." + resolvedType)) {
|
|
23689
|
+
resolvedType = key;
|
|
23690
|
+
break;
|
|
23498
23691
|
}
|
|
23499
23692
|
}
|
|
23693
|
+
if (resolvedType !== type) break;
|
|
23500
23694
|
}
|
|
23501
23695
|
}
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23507
|
-
|
|
23696
|
+
if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
|
|
23697
|
+
console.error("[execute-command] Handler NOT FOUND for type:", resolvedType);
|
|
23698
|
+
console.error(
|
|
23699
|
+
"[execute-command] Registered handlers:",
|
|
23700
|
+
[...handlersMap?.keys?.() ?? []].join(", ") || "(none)"
|
|
23701
|
+
);
|
|
23702
|
+
const matching = [...handlersMap?.keys?.() ?? []].filter((k) => k.includes("courseware"));
|
|
23703
|
+
console.error("[execute-command] Matching courseware keys:", matching.join(", ") || "(none)");
|
|
23704
|
+
}
|
|
23705
|
+
const cmd = await kernelContainer.commandBus.createCommand(
|
|
23706
|
+
resolvedType,
|
|
23707
|
+
payload ?? {},
|
|
23708
|
+
getActorId(req)
|
|
23709
|
+
);
|
|
23710
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23711
|
+
res.json({ success: true, result });
|
|
23712
|
+
} catch (err) {
|
|
23713
|
+
console.error("[execute-command]", err.message);
|
|
23714
|
+
res.status(500).json({ success: false, error: err.message });
|
|
23508
23715
|
}
|
|
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
23716
|
});
|
|
23525
|
-
|
|
23526
|
-
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23717
|
+
app.get("/api/ai-providers", (req, res) => {
|
|
23718
|
+
try {
|
|
23719
|
+
const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
|
|
23720
|
+
const masked = providers.map((p) => ({
|
|
23721
|
+
...p,
|
|
23722
|
+
api_key: maskApiKey(decryptApiKey(p.api_key || ""))
|
|
23723
|
+
}));
|
|
23724
|
+
res.json(masked);
|
|
23725
|
+
} catch (e) {
|
|
23726
|
+
res.status(500).json({ error: e.message });
|
|
23530
23727
|
}
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
23728
|
+
});
|
|
23729
|
+
app.post("/api/ai-providers", (req, res) => {
|
|
23730
|
+
try {
|
|
23731
|
+
const { name, api_url, api_key, model_name } = req.body;
|
|
23732
|
+
if (!name || !api_url || !model_name) {
|
|
23733
|
+
return res.status(400).json({ error: "Missing name, api_url or model_name" });
|
|
23734
|
+
}
|
|
23735
|
+
const id = "prov_" + Date.now();
|
|
23736
|
+
const now = Date.now();
|
|
23737
|
+
const encryptedKey = api_key ? encryptApiKey(api_key) : "";
|
|
23738
|
+
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);
|
|
23739
|
+
res.json({ success: true, id });
|
|
23740
|
+
} catch (e) {
|
|
23741
|
+
res.status(500).json({ error: e.message });
|
|
23535
23742
|
}
|
|
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
|
-
|
|
23743
|
+
});
|
|
23744
|
+
app.put("/api/ai-providers/:id", (req, res) => {
|
|
23745
|
+
try {
|
|
23746
|
+
const { name, api_url, api_key, model_name } = req.body;
|
|
23747
|
+
if (!name || !api_url || !model_name) {
|
|
23748
|
+
return res.status(400).json({ error: "Missing name, api_url or model_name" });
|
|
23749
|
+
}
|
|
23750
|
+
const now = Date.now();
|
|
23751
|
+
let finalKey;
|
|
23752
|
+
if (api_key && api_key.trim() !== "" && !api_key.includes("****")) {
|
|
23753
|
+
finalKey = encryptApiKey(api_key);
|
|
23754
|
+
} else {
|
|
23755
|
+
const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ?").get(req.params.id);
|
|
23756
|
+
finalKey = existing?.api_key || "";
|
|
23757
|
+
}
|
|
23758
|
+
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);
|
|
23759
|
+
res.json({ success: true });
|
|
23760
|
+
} catch (e) {
|
|
23761
|
+
res.status(500).json({ error: e.message });
|
|
23762
|
+
}
|
|
23763
|
+
});
|
|
23764
|
+
app.delete("/api/ai-providers/:id", (req, res) => {
|
|
23765
|
+
try {
|
|
23766
|
+
kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
|
|
23767
|
+
res.json({ success: true });
|
|
23768
|
+
} catch (e) {
|
|
23769
|
+
res.status(500).json({ error: e.message });
|
|
23770
|
+
}
|
|
23771
|
+
});
|
|
23772
|
+
app.get("/api/site-settings", (req, res) => {
|
|
23773
|
+
try {
|
|
23774
|
+
const row = kernelContainer.db.prepare("SELECT site_name, slogan, logo_url FROM site_settings WHERE id = ?").get("global");
|
|
23775
|
+
res.json({
|
|
23776
|
+
siteName: row?.site_name || "",
|
|
23777
|
+
slogan: row?.slogan || "",
|
|
23778
|
+
logoUrl: row?.logo_url || null
|
|
23779
|
+
});
|
|
23780
|
+
} catch (e) {
|
|
23781
|
+
res.status(500).json({ error: e.message });
|
|
23782
|
+
}
|
|
23783
|
+
});
|
|
23784
|
+
app.put("/api/site-settings", (req, res) => {
|
|
23785
|
+
try {
|
|
23786
|
+
const { siteName, slogan, logoUrl } = req.body || {};
|
|
23787
|
+
kernelContainer.db.prepare(
|
|
23788
|
+
`INSERT INTO site_settings (id, site_name, slogan, logo_url) VALUES ('global', ?, ?, ?)
|
|
23789
|
+
ON CONFLICT(id) DO UPDATE SET site_name = excluded.site_name, slogan = excluded.slogan, logo_url = excluded.logo_url`
|
|
23790
|
+
).run(siteName || "", slogan || "", logoUrl || null);
|
|
23791
|
+
res.json({
|
|
23792
|
+
success: true,
|
|
23793
|
+
siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
|
|
23794
|
+
});
|
|
23795
|
+
} catch (e) {
|
|
23796
|
+
res.status(500).json({ error: e.message });
|
|
23797
|
+
}
|
|
23798
|
+
});
|
|
23799
|
+
app.post("/api/ai-providers/test", async (req, res) => {
|
|
23800
|
+
try {
|
|
23801
|
+
const { api_url, api_key: providedKey, model_name } = req.body;
|
|
23802
|
+
if (!api_url || !model_name) {
|
|
23803
|
+
return res.status(400).json({ error: "api_url and model_name are required" });
|
|
23804
|
+
}
|
|
23805
|
+
let api_key = "";
|
|
23806
|
+
if (providedKey && providedKey.includes("****")) {
|
|
23807
|
+
const existing = kernelContainer.db.prepare(
|
|
23808
|
+
"SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? LIMIT 1"
|
|
23809
|
+
).get(api_url, model_name);
|
|
23810
|
+
api_key = existing ? decryptApiKey(existing.api_key) : "";
|
|
23811
|
+
} else if (providedKey) {
|
|
23812
|
+
api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
|
|
23813
|
+
}
|
|
23814
|
+
const controller = new AbortController();
|
|
23815
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
23816
|
+
let cleanUrl = api_url.trim();
|
|
23817
|
+
if (!cleanUrl.endsWith("/chat/completions")) {
|
|
23818
|
+
cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
|
|
23819
|
+
}
|
|
23820
|
+
const response = await fetch(cleanUrl, {
|
|
23821
|
+
method: "POST",
|
|
23822
|
+
headers: {
|
|
23823
|
+
"Content-Type": "application/json",
|
|
23824
|
+
"Authorization": `Bearer ${api_key || ""}`
|
|
23825
|
+
},
|
|
23826
|
+
body: JSON.stringify({
|
|
23827
|
+
model: model_name,
|
|
23828
|
+
messages: [{ role: "user", content: "Say connected" }],
|
|
23829
|
+
max_tokens: 5
|
|
23830
|
+
}),
|
|
23831
|
+
signal: controller.signal
|
|
23832
|
+
});
|
|
23833
|
+
clearTimeout(timeoutId);
|
|
23834
|
+
const responseText = await response.text();
|
|
23835
|
+
if (response.ok) {
|
|
23836
|
+
res.json({ success: true, message: "Successfully connected and received response." });
|
|
23837
|
+
} else {
|
|
23838
|
+
res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
|
|
23839
|
+
}
|
|
23840
|
+
} catch (e) {
|
|
23841
|
+
res.status(500).json({ success: false, error: `Connection failed: ${e.message}` });
|
|
23600
23842
|
}
|
|
23601
|
-
}
|
|
23602
|
-
|
|
23843
|
+
});
|
|
23844
|
+
}
|
|
23845
|
+
|
|
23846
|
+
// server.ts
|
|
23847
|
+
import_dotenv.default.config();
|
|
23848
|
+
if (!process.env.NODE_ENV) {
|
|
23849
|
+
const isCjs = typeof __filename !== "undefined" && __filename.endsWith(".cjs");
|
|
23850
|
+
const isDist = process.cwd().endsWith("/dist") || typeof __dirname !== "undefined" && __dirname.includes("/dist") || typeof __filename !== "undefined" && __filename.includes("/dist");
|
|
23851
|
+
if (isCjs || isDist) {
|
|
23852
|
+
process.env.NODE_ENV = "production";
|
|
23603
23853
|
}
|
|
23854
|
+
}
|
|
23855
|
+
async function startServer() {
|
|
23856
|
+
await ServerBootstrapAdapter.bootstrap({
|
|
23857
|
+
kernelContainer,
|
|
23858
|
+
environment: process.env.NODE_ENV || "development",
|
|
23859
|
+
config: { port: Number(process.env.PORT) || 9e3 }
|
|
23860
|
+
});
|
|
23861
|
+
await runStartupMigrations(kernelContainer.db);
|
|
23604
23862
|
await kernelContainer.ready;
|
|
23605
23863
|
const activityRegistry = new ActivityRegistry2();
|
|
23606
23864
|
registerOfficialActivities(activityRegistry, kernelContainer.actionRegistry);
|
|
@@ -23695,230 +23953,8 @@ async function startServer() {
|
|
|
23695
23953
|
registerSchedulesRoutes(ctx);
|
|
23696
23954
|
registerGradingRoutes(ctx);
|
|
23697
23955
|
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
|
-
});
|
|
23956
|
+
setupRealtimeBridge({ eventBus: kernelContainer.eventBus, io, db: kernelContainer.db });
|
|
23957
|
+
setupPresence({ io, eventBus: kernelContainer.eventBus });
|
|
23922
23958
|
if (process.env.NODE_ENV !== "production") {
|
|
23923
23959
|
const vite = await (0, import_vite.createServer)({
|
|
23924
23960
|
server: {
|