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/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 processService.registerInterval(name, intervalMs, (log) => {
1040
- try {
1041
- tickFn(log);
1042
- } catch (e) {
1043
- console.error(`[Plugin:${pluginId}] Error in interval task ${name}:`, e);
1044
- }
1045
- }).then((processId) => {
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, payload, state, log, updateState) => {
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 data... step ${i + 1}/${duration}`);
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 */ || previousStatus === "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 = (descriptor, instance) => {
16698
- if (serviceRegistry.has(descriptor.id)) {
16699
- serviceRegistry.replace(descriptor.id, instance);
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(descriptor, instance);
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
- // packages/core/bootstrap/adapter/bootstrap-registration.ts
17105
- var BootstrapRegistration = class {
17106
- static registerConfiguration(builder, context) {
17107
- if (context.config) {
17108
- builder.withConfiguration(context.config);
17109
- }
17110
- if (context.environment) {
17111
- builder.withEnvironment(context.environment);
17112
- }
17113
- }
17114
- static registerLogger(builder, context) {
17115
- if (context.logger) {
17116
- builder.withLogger(context.logger);
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
- static registerInfrastructure(builder, context) {
17120
- if (context.existingServices) {
17121
- for (const [id, service] of context.existingServices.entries()) {
17122
- builder.addService(id, service);
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
- if (context.kernelContainer) {
17126
- builder.addService("kernelContainer", context.kernelContainer);
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
- if (context.expressApp) {
17129
- builder.addService("expressApp", context.expressApp);
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
- if (context.httpServer) {
17132
- builder.addService("httpServer", context.httpServer);
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
- static registerExistingBootstrapStages(_builder) {
17136
- }
17137
- };
17138
-
17139
- // packages/core/bootstrap/adapter/server-bootstrap-adapter.ts
17140
- var ServerBootstrapAdapter = class _ServerBootstrapAdapter {
17141
- constructor(options) {
17142
- this._state = "Created";
17143
- this._state = "Created";
17144
- this._builder = PlatformBuilder.create();
17145
- if (options) {
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
- static create(options) {
17150
- return new _ServerBootstrapAdapter(options);
17151
- }
17152
- get state() {
17153
- return this._state;
17154
- }
17155
- configure(context) {
17156
- this._state = "Configuring";
17157
- BootstrapRegistration.registerConfiguration(this._builder, context);
17158
- BootstrapRegistration.registerLogger(this._builder, context);
17159
- BootstrapRegistration.registerInfrastructure(this._builder, context);
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
- if (this._state === "Configuring") {
17172
- this.registerStages();
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
- const builderResult = this._builder.buildResult();
17175
- const pipelineResult = await builderResult.pipeline.execute({
17176
- startupTimestamp: Date.now(),
17177
- startupOptions: {},
17178
- startupStage: builderResult.platformContext.currentStage,
17179
- startupToken: { token: "srv_adapter_token", isCancelled: false, cancel: () => {
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
- this._state = "Bootstrapped";
17195
- return { builderResult, pipelineResult };
17196
- }
17197
- static async bootstrap(context) {
17198
- const adapter = _ServerBootstrapAdapter.create(context);
17199
- return adapter.runBootstrap(context);
17200
- }
17201
- };
17252
+ });
17253
+ }
17202
17254
 
17203
- // packages/activity-ecosystem/index.ts
17204
- init_token();
17255
+ // server/shared-state.ts
17256
+ var MF_REMOTE_CACHE = /* @__PURE__ */ new Map();
17257
+ var lessonActiveSegments = /* @__PURE__ */ new Map();
17205
17258
 
17206
- // packages/activity-ecosystem/context.ts
17207
- function createActivityContext(opts) {
17208
- return {
17209
- commandBus: opts.commandBus,
17210
- eventBus: opts.eventBus,
17211
- actionRegistry: opts.actionRegistry,
17212
- capability: opts.capability,
17213
- ai: opts.ai,
17214
- classroom: opts.classroom ?? null
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
- // packages/activity-ecosystem/provider.ts
17219
- var import_uuid15 = require("uuid");
17220
- var ACTIVITY_EVENTS = {
17221
- REGISTERED: "activity.registered",
17222
- INITIALIZED: "activity.initialized",
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
- if (this.hooks.onInitialize) {
17265
- await this.hooks.onInitialize(context);
17266
- }
17267
- }
17268
- async start(context, payload) {
17269
- let result;
17270
- if (this.hooks.onStart) {
17271
- result = await this.hooks.onStart(context, payload);
17272
- } else if (this.descriptor.commandType) {
17273
- const command = await context.commandBus.createCommand(
17274
- this.descriptor.commandType,
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
- return result;
17297
- }
17298
- async pause(context) {
17299
- this._state = "paused";
17300
- publishEvent(context, ACTIVITY_EVENTS.PAUSED, this.descriptor, {
17301
- activityId: this.descriptor.id
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
- if (this.hooks.onPause) {
17304
- await this.hooks.onPause(context);
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
- if (this.hooks.onResume) {
17313
- await this.hooks.onResume(context);
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
- if (this.hooks.onFinish) {
17322
- await this.hooks.onFinish(context);
17323
- }
17324
- }
17325
- async dispose(context) {
17326
- this._state = "disposed";
17327
- publishEvent(context, ACTIVITY_EVENTS.DISPOSED, this.descriptor, {
17328
- activityId: this.descriptor.id
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
- if (this.hooks.onDispose) {
17331
- await this.hooks.onDispose(context);
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
- // packages/activity-ecosystem/registry.ts
17337
- var ActivityRegistry2 = class {
17338
- constructor() {
17339
- this.providers = /* @__PURE__ */ new Map();
17340
- }
17341
- /**
17342
- * Register an Activity Provider. Throws on missing id or duplicate id.
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
- this.providers.set(id, provider);
17354
- if (typeof provider.markRegistered === "function") {
17355
- provider.markRegistered();
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
- /** Remove a provider by id. Returns true if it was present. */
17359
- unregisterProvider(id) {
17360
- return this.providers.delete(id);
17361
- }
17362
- getProvider(id) {
17363
- return this.providers.get(id);
17364
- }
17365
- listProviders() {
17366
- return Array.from(this.providers.values());
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
- /** List the declarative descriptors (for Workspace catalogue / REST). */
17369
- listDescriptors() {
17370
- return this.listProviders().map((p) => p.descriptor);
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
- /** Activities visible to a given role (`all` matches everything). */
17373
- listByRole(role) {
17374
- return this.listProviders().filter(
17375
- (p) => p.descriptor.supportedRoles.includes("all") || p.descriptor.supportedRoles.includes(role)
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
- listByCategory(category) {
17379
- return this.listProviders().filter((p) => p.descriptor.category === category);
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
- * Execute an activity's `start` lifecycle on the supplied context.
17383
- *
17384
- * Permission isolation: when the provider declares `permissions`, at least
17385
- * one must be granted to `actorId` via the (reused) capability service,
17386
- * otherwise a PERMISSION_DENIED error is thrown (403 at the REST layer).
17387
- *
17388
- * The activity itself reuses the Command Bus / Event Bus this method only
17389
- * orchestrates the lifecycle and enforces permission.
17390
- */
17391
- async startActivity(id, context, payload, actorId) {
17392
- const provider = this.providers.get(id);
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
- const result = await provider.start(context, payload);
17411
- return {
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
- clear() {
17418
- this.providers.clear();
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
- // packages/activity-ecosystem/default-providers.ts
17423
- var OFFICIAL_PROVIDER = "official";
17424
- function aiActionFor(d) {
17425
- return {
17426
- id: `activity_ai_${d.id}`,
17427
- commandType: d.commandType ?? `activity.${d.id}.start`,
17428
- description: `Start the ${d.name} activity${d.description ? ` \u2014 ${d.description}` : ""}`,
17429
- inputSchema: {
17430
- type: "OBJECT",
17431
- properties: {
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
- function registerOfficialActivities(registry, actionRegistry) {
17570
- for (const def of OFFICIAL_ACTIVITY_DEFINITIONS) {
17571
- const descriptor = {
17572
- ...def,
17573
- aiAction: aiActionFor(def)
17574
- };
17575
- registry.registerProvider(new BaseActivityProvider({ descriptor }));
17576
- if (actionRegistry && descriptor.aiAction) {
17577
- actionRegistry.register(descriptor.aiAction);
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
- // packages/activity-ecosystem/index.ts
17583
- var IActivityRegistryToken = new Token(
17584
- "@openlearn/activity-ecosystem:IActivityRegistry"
17585
- );
17586
-
17587
- // server/routes/os.ts
17588
- var import_path8 = __toESM(require("path"), 1);
17589
- var import_fs8 = __toESM(require("fs"), 1);
17590
- var import_child_process = require("child_process");
17591
- var import_crypto6 = __toESM(require("crypto"), 1);
17592
- var import_xss = require("xss");
17593
-
17594
- // server/utils/crypto.ts
17595
- var import_crypto4 = __toESM(require("crypto"), 1);
17596
- var import_fs7 = __toESM(require("fs"), 1);
17597
- var import_path6 = __toESM(require("path"), 1);
17598
- var _encryptionKey = null;
17599
- function getEncryptionKey() {
17600
- if (_encryptionKey) return _encryptionKey;
17601
- const envPath = import_path6.default.resolve(process.cwd(), ".env");
17602
- const keyHex = process.env.ENCRYPTION_KEY;
17603
- if (keyHex && keyHex.trim() !== "") {
17604
- _encryptionKey = Buffer.from(keyHex.trim(), "hex");
17605
- return _encryptionKey;
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 content = import_fs7.default.readFileSync(envPath, "utf-8");
17610
- const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
17611
- if (match && match[1].trim() !== "") {
17612
- const fileKeyHex = match[1].trim();
17613
- _encryptionKey = Buffer.from(fileKeyHex, "hex");
17614
- process.env.ENCRYPTION_KEY = fileKeyHex;
17615
- console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
17616
- return _encryptionKey;
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
- const newKey = import_crypto4.default.randomBytes(32).toString("hex");
17622
- try {
17623
- if (import_fs7.default.existsSync(envPath)) {
17624
- let content = import_fs7.default.readFileSync(envPath, "utf-8");
17625
- if (/^ENCRYPTION_KEY=/m.test(content)) {
17626
- content = content.replace(/^ENCRYPTION_KEY=.*$/m, `ENCRYPTION_KEY=${newKey}`);
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
- process.env.ENCRYPTION_KEY = newKey;
17644
- _encryptionKey = Buffer.from(newKey, "hex");
17645
- return _encryptionKey;
17646
- }
17647
- function encryptApiKey(plaintext) {
17648
- if (!plaintext) return "";
17649
- const key = getEncryptionKey();
17650
- const iv = import_crypto4.default.randomBytes(16);
17651
- const cipher = import_crypto4.default.createCipheriv("aes-256-gcm", key, iv);
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
- function maskApiKey(key) {
17674
- if (!key || key.length <= 8) return key ? "****" : "";
17675
- return key.substring(0, 4) + "****" + key.substring(key.length - 4);
17676
- }
17677
- var PROMPT_INJECTION_PATTERNS = [
17678
- /ignore\s+(all\s+)?(previous|prior|above|system)\s+(instructions?|prompts?|directives?)/i,
17679
- /you\s+are\s+now\s+(a\s+)?(different|new|another)/i,
17680
- /forget\s+(all\s+)?(your|the)\s+(training|instructions?|rules?)/i,
17681
- /system\s*(prompt|message|instruction):\s*/i,
17682
- /<\|im_start\|>/i,
17683
- /<\|im_end\|>/i
17684
- ];
17685
- function detectPromptInjection(input) {
17686
- return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
17687
- }
17688
-
17689
- // server/middleware/auth.ts
17690
- function getValidSession(token) {
17691
- let sessionRow;
17692
- try {
17693
- sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
17694
- } catch {
17695
- return null;
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 (!sessionRow) return null;
17698
- const now = Date.now();
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
- const idleTimeout = 24 * 60 * 60 * 1e3;
17704
- if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
17705
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
17706
- return null;
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
- kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
17709
- return JSON.parse(sessionRow.session_data);
17710
- }
17711
- function getCookieToken(req) {
17712
- const rc = req.headers.cookie;
17713
- if (!rc) return null;
17714
- const parts = rc.split(";");
17715
- for (const part of parts) {
17716
- const trimmed = part.trim();
17717
- if (trimmed.startsWith("edu_os_token=")) {
17718
- return trimmed.substring("edu_os_token=".length);
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
- return null;
17722
- }
17723
- function getActorId(req) {
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
- if (role) {
17734
- return `user:${session.userId || "demo"}:${role}`;
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
- return "user-frontend";
17737
- } catch {
17738
- return "user-frontend";
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
- function checkIsTeacherOrAdmin(req) {
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
- // server/routes/shared.ts
17754
- var import_path7 = __toESM(require("path"), 1);
17755
- var import_crypto5 = __toESM(require("crypto"), 1);
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
- // server/utils/bridge-sdk.ts
17758
- var BRIDGE_SDK_CODE = `(function() {
17759
- // Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
17760
- try {
17761
- Object.defineProperty(document, 'cookie', {
17762
- get: function() { return ""; },
17763
- set: function(val) {},
17764
- configurable: true
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
- } catch (e) {
17767
- try {
17768
- Object.defineProperty(Document.prototype, 'cookie', {
17769
- get: function() { return ""; },
17770
- set: function(val) {},
17771
- configurable: true
17772
- });
17773
- } catch (err) {}
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
- // Proxy postMessage calls to enrich them with attempt_id/uuid and normalize targetOrigin
17777
- try {
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
- if (window.parent && window.parent !== window) {
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_" + import_crypto5.default.randomBytes(8).toString("hex");
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_" + import_crypto5.default.randomBytes(8).toString("hex");
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_" + import_crypto5.default.randomBytes(8).toString("hex");
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()}-${import_crypto6.default.randomBytes(4).toString("hex")}${ext}`;
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_" + import_crypto6.default.randomUUID(), convKey, "user", message, now);
18595
- kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
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 import_crypto8 = __toESM(require("crypto"), 1);
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_" + import_crypto8.default.randomBytes(8).toString("hex");
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_" + import_crypto8.default.randomBytes(8).toString("hex"),
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-" + import_crypto8.default.randomBytes(8).toString("hex");
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 import_genai2 = require("@google/genai");
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 import_genai2.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
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 import_crypto9 = __toESM(require("crypto"), 1);
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 = import_crypto9.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
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()}-${import_crypto9.default.randomBytes(4).toString("hex")}${ext}`;
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 = import_crypto9.default.createHash("sha256").update(providedPassword).digest("hex");
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_" + import_crypto9.default.randomBytes(16).toString("hex");
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 import_genai3 = require("@google/genai");
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 import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
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 import_genai3.GoogleGenAI({
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: import_genai3.Type.OBJECT,
22028
+ type: import_genai4.Type.OBJECT,
21384
22029
  properties: {
21385
22030
  learningObjectives: {
21386
- type: import_genai3.Type.ARRAY,
21387
- items: { type: import_genai3.Type.STRING },
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: import_genai3.Type.ARRAY,
22036
+ type: import_genai4.Type.ARRAY,
21392
22037
  items: {
21393
- type: import_genai3.Type.OBJECT,
22038
+ type: import_genai4.Type.OBJECT,
21394
22039
  properties: {
21395
- objective: { type: import_genai3.Type.STRING, description: "The specific learning objective tested by this question" },
21396
- question: { type: import_genai3.Type.STRING, description: "The multiple-choice question text" },
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: import_genai3.Type.ARRAY,
21399
- items: { type: import_genai3.Type.STRING },
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: import_genai3.Type.STRING, description: "The correct option (must exactly match one of the string options in the options array)" }
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 import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
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 import_genai4 = require("@google/genai");
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 import_genai4.GoogleGenAI({ apiKey: geminiKey });
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 import_genai5 = require("@google/genai");
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 import_genai5.GoogleGenAI({ apiKey: geminiKey });
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("[execute-command]", err.message);
23475
+ console.error(err);
23069
23476
  res.status(500).json({ success: false, error: err.message });
23070
23477
  }
23071
23478
  });
23072
- app.get("/api/ai-providers", (req, res) => {
23479
+ app.get("/api/plugins/:id(*)/contributions", (req, res) => {
23073
23480
  try {
23074
- const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
23075
- const masked = providers.map((p) => ({
23076
- ...p,
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.post("/api/ai-providers", (req, res) => {
23488
+ app.get("/api/plugins/:id(*)/config", (req, res) => {
23085
23489
  try {
23086
- const { name, api_url, api_key, model_name } = req.body;
23087
- if (!name || !api_url || !model_name) {
23088
- return res.status(400).json({ error: "Missing name, api_url or model_name" });
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 id = "prov_" + Date.now();
23091
- const now = Date.now();
23092
- const encryptedKey = api_key ? encryptApiKey(api_key) : "";
23093
- 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);
23094
- res.json({ success: true, id });
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.put("/api/ai-providers/:id", (req, res) => {
23508
+ app.post("/api/plugins/:id(*)/config", (req, res) => {
23100
23509
  try {
23101
- const { name, api_url, api_key, model_name } = req.body;
23102
- if (!name || !api_url || !model_name) {
23103
- return res.status(400).json({ error: "Missing name, api_url or model_name" });
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 now = Date.now();
23106
- let finalKey;
23107
- if (api_key && api_key.trim() !== "" && !api_key.includes("****")) {
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
- 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);
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.delete("/api/ai-providers/:id", (req, res) => {
23527
+ app.post("/api/plugins/:id(*)/toggle", async (req, res) => {
23120
23528
  try {
23121
- kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
23122
- res.json({ success: true });
23123
- } catch (e) {
23124
- res.status(500).json({ error: e.message });
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.get("/api/site-settings", (req, res) => {
23541
+ app.delete("/api/plugins/:id(*)", async (req, res) => {
23128
23542
  try {
23129
- const row = kernelContainer.db.prepare("SELECT site_name, slogan, logo_url FROM site_settings WHERE id = ?").get("global");
23130
- res.json({
23131
- siteName: row?.site_name || "",
23132
- slogan: row?.slogan || "",
23133
- logoUrl: row?.logo_url || null
23134
- });
23135
- } catch (e) {
23136
- res.status(500).json({ error: e.message });
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.put("/api/site-settings", (req, res) => {
23555
+ app.get("/api/plugins/:id(*)", async (req, res) => {
23140
23556
  try {
23141
- const { siteName, slogan, logoUrl } = req.body || {};
23142
- kernelContainer.db.prepare(
23143
- `INSERT INTO site_settings (id, site_name, slogan, logo_url) VALUES ('global', ?, ?, ?)
23144
- ON CONFLICT(id) DO UPDATE SET site_name = excluded.site_name, slogan = excluded.slogan, logo_url = excluded.logo_url`
23145
- ).run(siteName || "", slogan || "", logoUrl || null);
23146
- res.json({
23147
- success: true,
23148
- siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
23149
- });
23150
- } catch (e) {
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/ai-providers/test", async (req, res) => {
23569
+ app.post("/api/plugins", async (req, res) => {
23155
23570
  try {
23156
- const { api_url, api_key: providedKey, model_name } = req.body;
23157
- if (!api_url || !model_name) {
23158
- return res.status(400).json({ error: "api_url and model_name are required" });
23159
- }
23160
- let api_key = "";
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
- continue;
23267
- }
23268
- if (key === "items") {
23269
- normalized.items = normalizeToolSchema(value);
23270
- continue;
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 cmdResult = await kernelContainer.commandBus.execute(cmd);
23302
- actionResult = cmdResult;
23303
- allExecutedTools.push({ callName: toolName, success: true, result: cmdResult });
23304
- if (cmdResult && cmdResult.elementId && currentLessonId) {
23305
- const activeSeg = lessonActiveSegments.get(currentLessonId);
23306
- if (activeSeg) {
23307
- const row = kernelContainer.db.prepare("SELECT data FROM whiteboard_elements WHERE id = ?").get(cmdResult.elementId);
23308
- if (row) {
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
- actionResult = { error: err.message };
23332
- allExecutedTools.push({ callName: toolName, success: false, error: err.message });
23595
+ console.error(err);
23596
+ res.status(500).json({ success: false, error: err.message });
23333
23597
  }
23334
- } else {
23335
- actionResult = { error: `Command / Tool not found: ${toolName}` };
23336
- allExecutedTools.push({ callName: toolName, success: false, error: "Command not registered" });
23337
- }
23338
- return actionResult;
23339
- };
23340
- var buildOpenAIChatUrl = (apiUrl) => {
23341
- let cleanUrl = apiUrl.trim();
23342
- if (!cleanUrl.endsWith("/chat/completions")) {
23343
- cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
23344
- }
23345
- return cleanUrl;
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
- const candidate = response.candidates?.[0];
23379
- const contentParts = candidate?.content?.parts || [];
23380
- const functionCalls = contentParts.filter((p) => "functionCall" in p);
23381
- if (functionCalls.length === 0) {
23382
- finalResponseText = response.text || "";
23383
- break;
23384
- }
23385
- contents.push({
23386
- role: "model",
23387
- parts: contentParts
23388
- });
23389
- const toolParts = [];
23390
- for (const part of contentParts) {
23391
- if ("functionCall" in part && part.functionCall) {
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
- contents.push({
23414
- role: "tool",
23415
- parts: toolParts
23416
- });
23417
- loopCount++;
23418
- }
23419
- if (loopCount >= MAX_LOOPS && !finalResponseText) {
23420
- finalResponseText = "I have executed several internal commands to create or link resources, but reached the iteration limit. Please double-check the interface to confirm.";
23421
- }
23422
- return {
23423
- agentText: finalResponseText,
23424
- toolResults: allExecutedTools
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
- if (parsedArgs && typeof parsedArgs === "object" && attachments) {
23492
- for (const key of Object.keys(parsedArgs)) {
23493
- const val = parsedArgs[key];
23494
- if (typeof val === "string" && val.startsWith("ATTACHMENT_BASE64:")) {
23495
- const idx = parseInt(val.split(":")[1]);
23496
- if (attachments[idx]) {
23497
- parsedArgs[key] = attachments[idx].content;
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
- const actionResult = await executeAgentToolCall(toolName, parsedArgs, allExecutedTools, callerRole, currentLessonId);
23503
- messages.push({
23504
- role: "tool",
23505
- tool_call_id: call.id,
23506
- content: JSON.stringify(actionResult)
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
- try {
23526
- const existingQuiz = kernelContainer.db.prepare("SELECT id, manifest, source_code FROM plugins WHERE name = ?").get("Quiz Component Plugin");
23527
- if (existingQuiz && (!existingQuiz.manifest || !existingQuiz.manifest.includes("classroomTools") || !existingQuiz.source_code.includes("actorId:"))) {
23528
- console.log("Upgrading old Quiz Component Plugin to add classroomTools and fix Actor...");
23529
- kernelContainer.db.prepare("DELETE FROM plugins WHERE id = ?").run(existingQuiz.id);
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
- const existingRollCall = kernelContainer.db.prepare("SELECT id, manifest FROM plugins WHERE name = ?").get("Random Student Picker (\u968F\u673A\u70B9\u540D\u5C0F\u5DE5\uFFFD?)");
23532
- if (existingRollCall && (!existingRollCall.manifest || !existingRollCall.manifest.includes("classroomTools"))) {
23533
- console.log("Upgrading old Random Student Picker Plugin to add classroomTools...");
23534
- kernelContainer.db.prepare("DELETE FROM plugins WHERE id = ?").run(existingRollCall.id);
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
- } catch (e) {
23537
- console.error("Error upgrading old default plugins:", e);
23538
- }
23539
- try {
23540
- kernelContainer.db.exec(`
23541
- CREATE TABLE IF NOT EXISTS student_rollcalls (
23542
- id TEXT PRIMARY KEY,
23543
- student_id TEXT NOT NULL,
23544
- class_id TEXT,
23545
- lesson_id TEXT,
23546
- picked_time INTEGER NOT NULL
23547
- );
23548
- `);
23549
- console.log("student_rollcalls table successfully ensured.");
23550
- } catch (e) {
23551
- console.error("Error creating student_rollcalls table:", e);
23552
- }
23553
- try {
23554
- kernelContainer.db.exec(`
23555
- CREATE TABLE IF NOT EXISTS site_settings (
23556
- id TEXT PRIMARY KEY,
23557
- site_name TEXT,
23558
- slogan TEXT,
23559
- logo_url TEXT
23560
- );
23561
- `);
23562
- console.log("site_settings table successfully ensured.");
23563
- } catch (e) {
23564
- console.error("Error creating site_settings table:", e);
23565
- }
23566
- try {
23567
- kernelContainer.db.exec(`
23568
- CREATE TABLE IF NOT EXISTS agent_conversations (
23569
- id TEXT PRIMARY KEY,
23570
- conv_key TEXT NOT NULL,
23571
- role TEXT NOT NULL,
23572
- content TEXT NOT NULL,
23573
- created_at INTEGER NOT NULL
23574
- );
23575
- `);
23576
- kernelContainer.db.exec(
23577
- `CREATE INDEX IF NOT EXISTS idx_agent_conv_key ON agent_conversations(conv_key, created_at);`
23578
- );
23579
- console.log("agent_conversations table successfully ensured.");
23580
- } catch (e) {
23581
- console.error("Error creating agent_conversations table:", e);
23582
- }
23583
- try {
23584
- kernelContainer.db.exec(`ALTER TABLE client_sessions ADD COLUMN expires_at INTEGER`);
23585
- console.log("client_sessions.expires_at column ensured.");
23586
- } catch {
23587
- }
23588
- try {
23589
- const now = Date.now();
23590
- const idleTimeout = 24 * 60 * 60 * 1e3;
23591
- const deletedExpired = kernelContainer.db.prepare(
23592
- "DELETE FROM client_sessions WHERE expires_at IS NOT NULL AND expires_at < ?"
23593
- ).run(now);
23594
- const deletedIdle = kernelContainer.db.prepare(
23595
- "DELETE FROM client_sessions WHERE updated_at IS NOT NULL AND (? - updated_at) > ?"
23596
- ).run(now, idleTimeout);
23597
- const totalDeleted = (deletedExpired.changes || 0) + (deletedIdle.changes || 0);
23598
- if (totalDeleted > 0) {
23599
- console.log(`[Session] Cleaned up ${totalDeleted} expired sessions on startup.`);
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
- } catch (e) {
23602
- console.warn("[Session] Could not clean up expired sessions:", e);
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.subscribe("assignment.graded", (event) => {
23699
- try {
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: {