openlearn-next 0.1.16 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,996 +17110,1705 @@ function validateJsonSchema(data, schema) {
17096
17110
  }
17097
17111
 
17098
17112
  // server.ts
17099
- var import_genai6 = require("@google/genai");
17100
- var import_crypto13 = __toESM(require("crypto"), 1);
17101
17113
  var import_helmet = __toESM(require("helmet"), 1);
17102
17114
  var import_express_rate_limit = __toESM(require("express-rate-limit"), 1);
17103
17115
 
17104
- // 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);
17719
- }
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";
17668
+ const historyMessages = (history || []).map((h) => ({ role: h.role, content: h.content }));
17669
+ const messages = [
17670
+ { role: "system", content: systemInstruction },
17671
+ ...historyMessages,
17672
+ { role: "user", content: finalMessage }
17673
+ ];
17674
+ const allExecutedTools = [];
17675
+ let finalResponseText = "";
17676
+ const MAX_LOOPS = 5;
17677
+ let loopCount = 0;
17678
+ while (loopCount < MAX_LOOPS) {
17679
+ const response = await fetch(chatUrl, {
17680
+ method: "POST",
17681
+ headers,
17682
+ body: JSON.stringify({
17683
+ model: provider.model_name,
17684
+ messages,
17685
+ tools,
17686
+ tool_choice: tools.length > 0 ? "auto" : void 0,
17687
+ temperature: 0.1
17688
+ })
17689
+ });
17690
+ if (!response.ok) {
17691
+ const errorText = await response.text();
17692
+ throw new Error(`AI provider request failed (${response.status}): ${errorText || response.statusText}`);
17732
17693
  }
17733
- if (role) {
17734
- return `user:${session.userId || "demo"}:${role}`;
17694
+ const data = await response.json();
17695
+ const assistantMessage = data.choices?.[0]?.message;
17696
+ if (!assistantMessage) {
17697
+ throw new Error("AI provider returned no assistant message");
17735
17698
  }
17736
- return "user-frontend";
17737
- } catch {
17738
- return "user-frontend";
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;
17750
- }
17751
- }
17752
-
17753
- // server/routes/shared.ts
17754
- var import_path7 = __toESM(require("path"), 1);
17755
- var import_crypto5 = __toESM(require("crypto"), 1);
17756
-
17757
- // server/utils/bridge-sdk.ts
17758
- var BRIDGE_SDK_CODE = `(function() {
17759
- // Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
17760
- try {
17761
- Object.defineProperty(document, 'cookie', {
17762
- get: function() { return ""; },
17763
- set: function(val) {},
17764
- configurable: true
17699
+ finalResponseText = typeof assistantMessage.content === "string" ? assistantMessage.content.trim() : "";
17700
+ const toolCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
17701
+ messages.push({
17702
+ role: "assistant",
17703
+ content: assistantMessage.content ?? "",
17704
+ tool_calls: toolCalls
17765
17705
  });
17766
- } 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) {}
17774
- }
17775
-
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);
17706
+ if (toolCalls.length === 0) {
17707
+ break;
17708
+ }
17709
+ for (const call of toolCalls) {
17710
+ const toolName = call?.function?.name;
17711
+ if (!toolName) continue;
17712
+ let parsedArgs = {};
17713
+ if (typeof call?.function?.arguments === "string" && call.function.arguments.trim()) {
17714
+ try {
17715
+ parsedArgs = JSON.parse(call.function.arguments);
17716
+ } catch (err) {
17717
+ parsedArgs = {};
17800
17718
  }
17801
- throw err;
17802
17719
  }
17803
- };
17804
-
17805
- if (window.parent && window.parent !== window) {
17806
- const parentPostMessage = window.parent.postMessage;
17807
- try {
17808
- window.parent.postMessage = function(message, targetOrigin, transfer) {
17809
- try {
17810
- if (message && typeof message === 'object') {
17811
- if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
17812
- message.attempt_id = window.__LMS_STUDENT__.attempt_id;
17813
- }
17814
- if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
17815
- message.uuid = window.__LMS_COURSEWARE__.uuid;
17816
- }
17817
- }
17818
- } catch (e) {}
17819
-
17820
- let origin = targetOrigin;
17821
- if (origin === 'null') {
17822
- origin = '*';
17823
- }
17824
- try {
17825
- return parentPostMessage.call(window.parent, message, origin, transfer);
17826
- } catch (err) {
17827
- if (err.name === 'SyntaxError' && origin !== '*') {
17828
- return parentPostMessage.call(window.parent, message, '*', transfer);
17720
+ if (parsedArgs && typeof parsedArgs === "object" && attachments) {
17721
+ for (const key of Object.keys(parsedArgs)) {
17722
+ const val = parsedArgs[key];
17723
+ if (typeof val === "string" && val.startsWith("ATTACHMENT_BASE64:")) {
17724
+ const idx = parseInt(val.split(":")[1]);
17725
+ if (attachments[idx]) {
17726
+ parsedArgs[key] = attachments[idx].content;
17829
17727
  }
17830
- throw err;
17831
17728
  }
17832
- };
17833
- } catch (e) {}
17834
- }
17835
- } catch (e) {}
17836
-
17837
- window.LMS = {
17838
- submit(data) {
17839
- window.parent.postMessage({
17840
- type: "LMS_SUBMIT",
17841
- uuid: window.__LMS_COURSEWARE__?.uuid,
17842
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17843
- payload: data
17844
- }, "*");
17845
- },
17846
- saveProgress(data) {
17847
- window.parent.postMessage({
17848
- type: "LMS_SAVE_PROGRESS",
17849
- uuid: window.__LMS_COURSEWARE__?.uuid,
17850
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17851
- payload: data
17852
- }, "*");
17853
- },
17854
- finish(data) {
17855
- window.parent.postMessage({
17856
- type: "LMS_FINISH",
17857
- uuid: window.__LMS_COURSEWARE__?.uuid,
17858
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17859
- payload: data
17860
- }, "*");
17861
- },
17862
- getStudent() {
17863
- return window.__LMS_STUDENT__;
17864
- },
17865
- getCourseware() {
17866
- return window.__LMS_COURSEWARE__;
17867
- },
17868
- log(event, data) {
17869
- window.parent.postMessage({
17870
- type: "LMS_LOG",
17871
- uuid: window.__LMS_COURSEWARE__?.uuid,
17872
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17873
- event: event,
17874
- payload: data
17875
- }, "*");
17729
+ }
17730
+ }
17731
+ const actionResult = await executeAgentToolCall(toolName, parsedArgs, allExecutedTools, callerRole, currentLessonId);
17732
+ messages.push({
17733
+ role: "tool",
17734
+ tool_call_id: call.id,
17735
+ content: JSON.stringify(actionResult)
17736
+ });
17876
17737
  }
17738
+ loopCount++;
17739
+ }
17740
+ if (loopCount >= MAX_LOOPS && !finalResponseText) {
17741
+ finalResponseText = "I have executed several internal commands, but reached the iteration limit. Please review the assistant panel for the latest state.";
17742
+ }
17743
+ return {
17744
+ agentText: finalResponseText,
17745
+ toolResults: allExecutedTools
17877
17746
  };
17747
+ };
17878
17748
 
17879
- try {
17880
- if (window.fetch) {
17881
- const originalFetch = window.fetch;
17882
- window.fetch = function(input, init) {
17883
- try {
17884
- const url = (typeof input === 'string') ? input : (input?.url || '');
17885
- const method = init?.method || input?.method || 'GET';
17886
- const headers = init?.headers || input?.headers || {};
17887
- let body = init?.body || input?.body || null;
17888
-
17889
- if (body && typeof body === 'object') {
17890
- try { body = JSON.stringify(body); } catch(e){}
17891
- }
17892
-
17893
- if (url && !url.includes('/api/courseware/attempts/')) {
17894
- window.parent.postMessage({
17895
- type: "HOOK_FETCH",
17896
- uuid: window.__LMS_COURSEWARE__?.uuid,
17897
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17898
- payload: { url, method, headers: JSON.parse(JSON.stringify(headers)), body: body ? body.toString() : null }
17899
- }, "*");
17900
- }
17901
- } catch (e) {
17902
- console.error("Bridge Hook fetch error", e);
17903
- }
17904
- return originalFetch.apply(this, arguments);
17905
- };
17749
+ // packages/core/bootstrap/adapter/bootstrap-registration.ts
17750
+ var BootstrapRegistration = class {
17751
+ static registerConfiguration(builder, context) {
17752
+ if (context.config) {
17753
+ builder.withConfiguration(context.config);
17906
17754
  }
17907
-
17908
- if (window.XMLHttpRequest) {
17909
- const originalOpen = XMLHttpRequest.prototype.open;
17910
- const originalSend = XMLHttpRequest.prototype.send;
17911
- XMLHttpRequest.prototype.open = function(method, url) {
17912
- this._method = method;
17913
- this._url = url;
17914
- return originalOpen.apply(this, arguments);
17915
- };
17916
- XMLHttpRequest.prototype.send = function(body) {
17917
- try {
17918
- let bodyStr = body;
17919
- if (body && typeof body === 'object') {
17920
- try { bodyStr = JSON.stringify(body); } catch(e){}
17921
- }
17922
- if (this._url && !this._url.includes('/api/courseware/attempts/')) {
17923
- window.parent.postMessage({
17924
- type: "HOOK_XHR",
17925
- uuid: window.__LMS_COURSEWARE__?.uuid,
17926
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17927
- payload: { url: this._url, method: this._method, body: bodyStr ? bodyStr.toString() : null }
17928
- }, "*");
17929
- }
17930
- } catch (e) {
17931
- console.error("Bridge Hook XHR error", e);
17932
- }
17933
- return originalSend.apply(this, arguments);
17934
- };
17755
+ if (context.environment) {
17756
+ builder.withEnvironment(context.environment);
17935
17757
  }
17936
-
17937
- function attachToAxios(axiosInstance) {
17938
- if (axiosInstance && axiosInstance.interceptors && axiosInstance.interceptors.request) {
17939
- axiosInstance.interceptors.request.use(function(config) {
17940
- try {
17941
- if (config.url && !config.url.includes('/api/courseware/attempts/')) {
17942
- window.parent.postMessage({
17943
- type: "HOOK_AXIOS",
17944
- uuid: window.__LMS_COURSEWARE__?.uuid,
17945
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17946
- payload: { url: config.url, method: config.method, data: config.data }
17947
- }, "*");
17948
- }
17949
- } catch (e) {
17950
- console.error("Bridge Hook Axios error", e);
17951
- }
17952
- return config;
17953
- }, function(error) { return Promise.reject(error); });
17758
+ }
17759
+ static registerLogger(builder, context) {
17760
+ if (context.logger) {
17761
+ builder.withLogger(context.logger);
17762
+ }
17763
+ }
17764
+ static registerInfrastructure(builder, context) {
17765
+ if (context.existingServices) {
17766
+ for (const [id, service] of context.existingServices.entries()) {
17767
+ builder.addService(id, service);
17954
17768
  }
17955
17769
  }
17956
- if (window.axios) {
17957
- attachToAxios(window.axios);
17770
+ if (context.kernelContainer) {
17771
+ builder.addService("kernelContainer", context.kernelContainer);
17958
17772
  }
17959
- var _axios = window.axios;
17960
- Object.defineProperty(window, 'axios', {
17961
- get: function() { return _axios; },
17962
- set: function(val) {
17963
- _axios = val;
17964
- attachToAxios(val);
17965
- },
17966
- configurable: true
17967
- });
17968
-
17969
- if (navigator && navigator.sendBeacon) {
17970
- const originalSendBeacon = navigator.sendBeacon;
17971
- navigator.sendBeacon = function(url, data) {
17972
- try {
17973
- if (url && !url.includes('/api/courseware/attempts/')) {
17974
- window.parent.postMessage({
17975
- type: "HOOK_BEACON",
17976
- uuid: window.__LMS_COURSEWARE__?.uuid,
17977
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
17978
- payload: { url: url, data: data ? data.toString() : null }
17979
- }, "*");
17980
- }
17981
- } catch (e) {
17982
- console.error("Bridge Hook Beacon error", e);
17983
- }
17984
- return originalSendBeacon.apply(this, arguments);
17985
- };
17773
+ if (context.expressApp) {
17774
+ builder.addService("expressApp", context.expressApp);
17775
+ }
17776
+ if (context.httpServer) {
17777
+ builder.addService("httpServer", context.httpServer);
17986
17778
  }
17779
+ }
17780
+ static registerExistingBootstrapStages(_builder) {
17781
+ }
17782
+ };
17987
17783
 
17988
- window.addEventListener('submit', function(e) {
17989
- try {
17990
- const form = e.target;
17991
- const formData = new FormData(form);
17992
- const data = {};
17993
- formData.forEach(function(value, key) {
17994
- data[key] = value;
17995
- });
17996
- if (form.action && !form.action.includes('/api/courseware/attempts/')) {
17997
- window.parent.postMessage({
17998
- type: "HOOK_FORM",
17999
- uuid: window.__LMS_COURSEWARE__?.uuid,
18000
- attempt_id: window.__LMS_STUDENT__?.attempt_id,
18001
- payload: { action: form.action, method: form.method, data: data }
18002
- }, "*");
18003
- }
18004
- } catch (err) {
18005
- console.error("Bridge Hook Form error", err);
17784
+ // packages/core/bootstrap/adapter/server-bootstrap-adapter.ts
17785
+ var ServerBootstrapAdapter = class _ServerBootstrapAdapter {
17786
+ constructor(options) {
17787
+ this._state = "Created";
17788
+ this._state = "Created";
17789
+ this._builder = PlatformBuilder.create();
17790
+ if (options) {
17791
+ this.configure(options);
17792
+ }
17793
+ }
17794
+ static create(options) {
17795
+ return new _ServerBootstrapAdapter(options);
17796
+ }
17797
+ get state() {
17798
+ return this._state;
17799
+ }
17800
+ configure(context) {
17801
+ this._state = "Configuring";
17802
+ BootstrapRegistration.registerConfiguration(this._builder, context);
17803
+ BootstrapRegistration.registerLogger(this._builder, context);
17804
+ BootstrapRegistration.registerInfrastructure(this._builder, context);
17805
+ return this;
17806
+ }
17807
+ registerStages() {
17808
+ BootstrapRegistration.registerExistingBootstrapStages(this._builder);
17809
+ this._state = "PipelineRegistered";
17810
+ return this;
17811
+ }
17812
+ async runBootstrap(context) {
17813
+ if (this._state === "Created") {
17814
+ this.configure(context);
17815
+ }
17816
+ if (this._state === "Configuring") {
17817
+ this.registerStages();
17818
+ }
17819
+ const builderResult = this._builder.buildResult();
17820
+ const pipelineResult = await builderResult.pipeline.execute({
17821
+ startupTimestamp: Date.now(),
17822
+ startupOptions: {},
17823
+ startupStage: builderResult.platformContext.currentStage,
17824
+ startupToken: { token: "srv_adapter_token", isCancelled: false, cancel: () => {
17825
+ } },
17826
+ isCancelled: false,
17827
+ platformContext: builderResult.platformContext,
17828
+ config: builderResult.platformContext.config,
17829
+ state: "Active",
17830
+ currentStage: builderResult.platformContext.currentStage,
17831
+ startTime: Date.now(),
17832
+ getMetadata: () => void 0,
17833
+ setStage: () => {
18006
17834
  }
18007
- }, true);
18008
-
18009
- // --- SMART DOM SCRAPER FOR GENERIC COURSEWARES ---
18010
- function logToServer(msg, detail) {
18011
- // \u4EC5 console \u8F93\u51FA\uFF1B\u7F51\u7EDC\u8BF7\u6C42\u53EF\u80FD\u88AB\u6D4F\u89C8\u5668 HTTPS \u5347\u7EA7\u5BFC\u81F4 ERR_CONNECTION_REFUSED
18012
- try {
18013
- console.log('[LMS Debug]', msg, detail || '');
18014
- } catch (e) {}
17835
+ });
17836
+ if (pipelineResult.status === "Failed") {
17837
+ throw pipelineResult.error || new Error(`ServerBootstrapAdapter pipeline failed at stage: ${pipelineResult.failedStage}`);
18015
17838
  }
17839
+ this._state = "Bootstrapped";
17840
+ return { builderResult, pipelineResult };
17841
+ }
17842
+ static async bootstrap(context) {
17843
+ const adapter = _ServerBootstrapAdapter.create(context);
17844
+ return adapter.runBootstrap(context);
17845
+ }
17846
+ };
18016
17847
 
18017
- function findScoreInDOM() {
18018
- const logData = [];
18019
- try {
18020
- const commonVars = ['score', 'points', 'grade', 'totalScore', 'currentScore', 'userScore', 'finalScore', 'correctCount'];
18021
- for (const v of commonVars) {
18022
- if (typeof window[v] === 'number') {
18023
- logData.push("Global var " + v + " is number: " + window[v]);
18024
- return { score: window[v], log: logData };
18025
- }
18026
- if (typeof window[v] === 'string') {
18027
- const num = parseFloat(window[v]);
18028
- if (!isNaN(num)) {
18029
- logData.push("Global var " + v + " is string with number: " + window[v]);
18030
- return { score: num, log: logData };
18031
- }
18032
- }
18033
- }
18034
-
18035
- const selectors = [
18036
- '#score', '#scoreDisplay', '#score-num', '#scoreDisplaySpan', '#points', '#grade',
18037
- '.score', '.points', '.grade', '.score-num', '.score-value',
18038
- '[id*="score" i]', '[id*="point" i]', '[id*="grade" i]', '[id*="result" i]',
18039
- '[class*="score" i]', '[class*="point" i]', '[class*="grade" i]', '[class*="result" i]'
18040
- ];
18041
-
18042
- for (const selector of selectors) {
18043
- try {
18044
- const el = document.querySelector(selector);
18045
- if (el) {
18046
- const text = (el.textContent || el.innerText || '').trim();
18047
- if (text) {
18048
- logData.push("Selector '" + selector + "' matched text: '" + text + "'");
18049
- const fractionMatch = text.match(/(\\\\d+(\\\\.\\\\d+)?)\\\\s*[\\\\/|\u4E4B]\\\\s*(\\\\d+)/);
18050
- if (fractionMatch) {
18051
- const num = parseFloat(fractionMatch[1]);
18052
- const den = parseFloat(fractionMatch[3]);
18053
- if (den > 0) {
18054
- const pct = (num / den) * 100;
18055
- logData.push("Parsed fraction: " + num + "/" + den + " -> " + pct);
18056
- return { score: pct, log: logData };
18057
- }
18058
- }
18059
- const match = text.match(/\\\\d+(\\\\.\\\\d+)?/);
18060
- if (match) {
18061
- const num = parseFloat(match[0]);
18062
- if (!isNaN(num)) {
18063
- logData.push("Parsed decimal: " + num);
18064
- return { score: num, log: logData };
18065
- }
18066
- }
18067
- }
18068
- }
18069
- } catch (e) {}
18070
- }
17848
+ // packages/activity-ecosystem/index.ts
17849
+ init_token();
18071
17850
 
18072
- try {
18073
- const inputs = document.querySelectorAll('input[type="text"], input[type="number"], input[readonly]');
18074
- for (const input of inputs) {
18075
- const id = (input.id || '').toLowerCase();
18076
- const name = (input.name || '').toLowerCase();
18077
- if (id.includes('score') || name.includes('score') || id.includes('point') || name.includes('point')) {
18078
- const val = parseFloat(input.value);
18079
- if (!isNaN(val)) {
18080
- logData.push("Input id=" + id + " name=" + name + " value: " + input.value);
18081
- return { score: val, log: logData };
18082
- }
18083
- }
18084
- }
18085
- } catch (e) {}
17851
+ // packages/activity-ecosystem/context.ts
17852
+ function createActivityContext(opts) {
17853
+ return {
17854
+ commandBus: opts.commandBus,
17855
+ eventBus: opts.eventBus,
17856
+ actionRegistry: opts.actionRegistry,
17857
+ capability: opts.capability,
17858
+ ai: opts.ai,
17859
+ classroom: opts.classroom ?? null
17860
+ };
17861
+ }
18086
17862
 
18087
- try {
18088
- const all = document.getElementsByTagName('*');
17863
+ // packages/activity-ecosystem/provider.ts
17864
+ var import_uuid15 = require("uuid");
17865
+ var ACTIVITY_EVENTS = {
17866
+ REGISTERED: "activity.registered",
17867
+ INITIALIZED: "activity.initialized",
17868
+ STARTED: "activity.started",
17869
+ PAUSED: "activity.paused",
17870
+ RESUMED: "activity.resumed",
17871
+ FINISHED: "activity.finished",
17872
+ DISPOSED: "activity.disposed"
17873
+ };
17874
+ function publishEvent(context, type, descriptor, payload) {
17875
+ context.eventBus.publish({
17876
+ id: (0, import_uuid15.v7)(),
17877
+ type,
17878
+ source: `activity:${descriptor.id}`,
17879
+ payload,
17880
+ timestamp: Date.now()
17881
+ });
17882
+ }
17883
+ var BaseActivityProvider = class {
17884
+ constructor(options) {
17885
+ this._state = "registered";
17886
+ if (!options?.descriptor?.id) {
17887
+ throw new Error("BaseActivityProvider requires a descriptor with a valid id.");
17888
+ }
17889
+ this.descriptor = options.descriptor;
17890
+ this.hooks = options;
17891
+ }
17892
+ get state() {
17893
+ return this._state;
17894
+ }
17895
+ /** When the activity entered `running` (undefined before first start). */
17896
+ get startedAt() {
17897
+ return this._startedAt;
17898
+ }
17899
+ /** Internal marker used by the registry on registration. */
17900
+ markRegistered() {
17901
+ this._state = "registered";
17902
+ }
17903
+ async initialize(context) {
17904
+ this._state = "initialized";
17905
+ publishEvent(context, ACTIVITY_EVENTS.INITIALIZED, this.descriptor, {
17906
+ activityId: this.descriptor.id,
17907
+ provider: this.descriptor.provider
17908
+ });
17909
+ if (this.hooks.onInitialize) {
17910
+ await this.hooks.onInitialize(context);
17911
+ }
17912
+ }
17913
+ async start(context, payload) {
17914
+ let result;
17915
+ if (this.hooks.onStart) {
17916
+ result = await this.hooks.onStart(context, payload);
17917
+ } else if (this.descriptor.commandType) {
17918
+ const command = await context.commandBus.createCommand(
17919
+ this.descriptor.commandType,
17920
+ payload ?? {},
17921
+ this.descriptor.provider,
17922
+ { activityId: this.descriptor.id }
17923
+ );
17924
+ try {
17925
+ result = await context.commandBus.execute(command);
17926
+ } catch (err) {
17927
+ const message = err instanceof Error ? err.message : String(err);
17928
+ if (!/No handler registered for command/.test(message)) {
17929
+ throw err;
17930
+ }
17931
+ }
17932
+ }
17933
+ this._state = "running";
17934
+ this._startedAt = Date.now();
17935
+ publishEvent(context, ACTIVITY_EVENTS.STARTED, this.descriptor, {
17936
+ activityId: this.descriptor.id,
17937
+ provider: this.descriptor.provider,
17938
+ commandType: this.descriptor.commandType,
17939
+ payload
17940
+ });
17941
+ return result;
17942
+ }
17943
+ async pause(context) {
17944
+ this._state = "paused";
17945
+ publishEvent(context, ACTIVITY_EVENTS.PAUSED, this.descriptor, {
17946
+ activityId: this.descriptor.id
17947
+ });
17948
+ if (this.hooks.onPause) {
17949
+ await this.hooks.onPause(context);
17950
+ }
17951
+ }
17952
+ async resume(context) {
17953
+ this._state = "running";
17954
+ publishEvent(context, ACTIVITY_EVENTS.RESUMED, this.descriptor, {
17955
+ activityId: this.descriptor.id
17956
+ });
17957
+ if (this.hooks.onResume) {
17958
+ await this.hooks.onResume(context);
17959
+ }
17960
+ }
17961
+ async finish(context) {
17962
+ this._state = "finished";
17963
+ publishEvent(context, ACTIVITY_EVENTS.FINISHED, this.descriptor, {
17964
+ activityId: this.descriptor.id
17965
+ });
17966
+ if (this.hooks.onFinish) {
17967
+ await this.hooks.onFinish(context);
17968
+ }
17969
+ }
17970
+ async dispose(context) {
17971
+ this._state = "disposed";
17972
+ publishEvent(context, ACTIVITY_EVENTS.DISPOSED, this.descriptor, {
17973
+ activityId: this.descriptor.id
17974
+ });
17975
+ if (this.hooks.onDispose) {
17976
+ await this.hooks.onDispose(context);
17977
+ }
17978
+ }
17979
+ };
17980
+
17981
+ // packages/activity-ecosystem/registry.ts
17982
+ var ActivityRegistry2 = class {
17983
+ constructor() {
17984
+ this.providers = /* @__PURE__ */ new Map();
17985
+ }
17986
+ /**
17987
+ * Register an Activity Provider. Throws on missing id or duplicate id.
17988
+ * Official and plugin providers go through this identical method.
17989
+ */
17990
+ registerProvider(provider) {
17991
+ if (!provider || !provider.descriptor || !provider.descriptor.id) {
17992
+ throw new Error("ActivityRegistry: an ActivityProvider must expose a descriptor with a valid id.");
17993
+ }
17994
+ const id = provider.descriptor.id;
17995
+ if (this.providers.has(id)) {
17996
+ throw new Error(`ActivityRegistry: activity provider "${id}" is already registered.`);
17997
+ }
17998
+ this.providers.set(id, provider);
17999
+ if (typeof provider.markRegistered === "function") {
18000
+ provider.markRegistered();
18001
+ }
18002
+ }
18003
+ /** Remove a provider by id. Returns true if it was present. */
18004
+ unregisterProvider(id) {
18005
+ return this.providers.delete(id);
18006
+ }
18007
+ getProvider(id) {
18008
+ return this.providers.get(id);
18009
+ }
18010
+ listProviders() {
18011
+ return Array.from(this.providers.values());
18012
+ }
18013
+ /** List the declarative descriptors (for Workspace catalogue / REST). */
18014
+ listDescriptors() {
18015
+ return this.listProviders().map((p) => p.descriptor);
18016
+ }
18017
+ /** Activities visible to a given role (`all` matches everything). */
18018
+ listByRole(role) {
18019
+ return this.listProviders().filter(
18020
+ (p) => p.descriptor.supportedRoles.includes("all") || p.descriptor.supportedRoles.includes(role)
18021
+ );
18022
+ }
18023
+ listByCategory(category) {
18024
+ return this.listProviders().filter((p) => p.descriptor.category === category);
18025
+ }
18026
+ /**
18027
+ * Execute an activity's `start` lifecycle on the supplied context.
18028
+ *
18029
+ * Permission isolation: when the provider declares `permissions`, at least
18030
+ * one must be granted to `actorId` via the (reused) capability service,
18031
+ * otherwise a PERMISSION_DENIED error is thrown (403 at the REST layer).
18032
+ *
18033
+ * The activity itself reuses the Command Bus / Event Bus — this method only
18034
+ * orchestrates the lifecycle and enforces permission.
18035
+ */
18036
+ async startActivity(id, context, payload, actorId) {
18037
+ const provider = this.providers.get(id);
18038
+ if (!provider) {
18039
+ throw new Error(`ActivityRegistry: activity provider "${id}" not found.`);
18040
+ }
18041
+ const required = provider.descriptor.permissions ?? [];
18042
+ if (required.length > 0 && actorId) {
18043
+ const results = await Promise.all(
18044
+ required.map((cap) => context.capability.check(actorId, cap))
18045
+ );
18046
+ const granted = results.some(Boolean);
18047
+ if (!granted) {
18048
+ const err = new Error(
18049
+ `[ActivityPermission] Actor "${actorId}" is missing a required permission (${required.join(", ")}) for activity "${id}".`
18050
+ );
18051
+ err.code = "PERMISSION_DENIED";
18052
+ throw err;
18053
+ }
18054
+ }
18055
+ const result = await provider.start(context, payload);
18056
+ return {
18057
+ provider: provider.descriptor.provider,
18058
+ dispatched: Boolean(provider.descriptor.commandType),
18059
+ result
18060
+ };
18061
+ }
18062
+ clear() {
18063
+ this.providers.clear();
18064
+ }
18065
+ };
18066
+
18067
+ // packages/activity-ecosystem/default-providers.ts
18068
+ var OFFICIAL_PROVIDER = "official";
18069
+ function aiActionFor(d) {
18070
+ return {
18071
+ id: `activity_ai_${d.id}`,
18072
+ commandType: d.commandType ?? `activity.${d.id}.start`,
18073
+ description: `Start the ${d.name} activity${d.description ? ` \u2014 ${d.description}` : ""}`,
18074
+ inputSchema: {
18075
+ type: "OBJECT",
18076
+ properties: {
18077
+ payload: {
18078
+ type: "OBJECT",
18079
+ description: `Optional configuration for the ${d.name} activity`
18080
+ }
18081
+ }
18082
+ },
18083
+ capabilityRequired: d.permissions?.[0] ?? "lesson:read"
18084
+ };
18085
+ }
18086
+ var OFFICIAL_ACTIVITY_DEFINITIONS = [
18087
+ {
18088
+ id: "official_quiz",
18089
+ name: "Quiz",
18090
+ description: "In-class multiple choice / graded quiz.",
18091
+ icon: "HelpCircle",
18092
+ category: "assessment",
18093
+ permissions: ["quiz:write"],
18094
+ supportedRoles: ["teacher", "student", "all"],
18095
+ supportedDevices: ["desktop", "tablet", "all"],
18096
+ tags: ["assessment", "quiz"],
18097
+ version: "1.0.0",
18098
+ provider: OFFICIAL_PROVIDER,
18099
+ commandType: "quiz.create"
18100
+ },
18101
+ {
18102
+ id: "official_vote",
18103
+ name: "Vote",
18104
+ description: "Quick classroom vote / poll on a question.",
18105
+ icon: "Vote",
18106
+ category: "engagement",
18107
+ permissions: ["lesson:control"],
18108
+ supportedRoles: ["teacher", "student", "all"],
18109
+ supportedDevices: ["desktop", "tablet", "mobile", "all"],
18110
+ tags: ["engagement", "vote", "poll"],
18111
+ version: "1.0.0",
18112
+ provider: OFFICIAL_PROVIDER,
18113
+ commandType: "vote.create"
18114
+ },
18115
+ {
18116
+ id: "official_poll",
18117
+ name: "Poll",
18118
+ description: "Live polling with instant results.",
18119
+ icon: "BarChart3",
18120
+ category: "engagement",
18121
+ permissions: ["lesson:control"],
18122
+ supportedRoles: ["teacher", "student", "all"],
18123
+ supportedDevices: ["desktop", "tablet", "mobile", "all"],
18124
+ tags: ["engagement", "poll"],
18125
+ version: "1.0.0",
18126
+ provider: OFFICIAL_PROVIDER,
18127
+ commandType: "poll.create"
18128
+ },
18129
+ {
18130
+ id: "official_discussion",
18131
+ name: "Discussion",
18132
+ description: "Whole-class or group real-time discussion thread.",
18133
+ icon: "MessagesSquare",
18134
+ category: "collaboration",
18135
+ permissions: ["lesson:control"],
18136
+ supportedRoles: ["teacher", "student", "all"],
18137
+ supportedDevices: ["desktop", "tablet", "all"],
18138
+ tags: ["collaboration", "discussion"],
18139
+ version: "1.0.0",
18140
+ provider: OFFICIAL_PROVIDER,
18141
+ commandType: "discussion.create"
18142
+ },
18143
+ {
18144
+ id: "official_grouping",
18145
+ name: "Grouping",
18146
+ description: "Auto / random / manual student grouping.",
18147
+ icon: "Users",
18148
+ category: "collaboration",
18149
+ permissions: ["lesson:control"],
18150
+ supportedRoles: ["teacher"],
18151
+ supportedDevices: ["desktop", "all"],
18152
+ tags: ["collaboration", "group"],
18153
+ version: "1.0.0",
18154
+ provider: OFFICIAL_PROVIDER,
18155
+ commandType: "grouping.create"
18156
+ },
18157
+ {
18158
+ id: "official_assignment",
18159
+ name: "Assignment",
18160
+ description: "Create, submit and grade student assignments / homework.",
18161
+ icon: "FileText",
18162
+ category: "management",
18163
+ permissions: ["management:write"],
18164
+ supportedRoles: ["teacher", "student", "all"],
18165
+ supportedDevices: ["desktop", "tablet", "all"],
18166
+ tags: ["management", "assignment", "homework"],
18167
+ version: "1.0.0",
18168
+ provider: OFFICIAL_PROVIDER,
18169
+ commandType: "assignment.create"
18170
+ },
18171
+ {
18172
+ id: "official_competition",
18173
+ name: "Competition",
18174
+ description: "Timed leaderboard competition between students / groups.",
18175
+ icon: "Trophy",
18176
+ category: "engagement",
18177
+ permissions: ["lesson:control"],
18178
+ supportedRoles: ["teacher", "student", "all"],
18179
+ supportedDevices: ["desktop", "tablet", "mobile", "all"],
18180
+ tags: ["engagement", "competition", "gamification"],
18181
+ version: "1.0.0",
18182
+ provider: OFFICIAL_PROVIDER,
18183
+ commandType: "competition.create"
18184
+ },
18185
+ {
18186
+ id: "official_checkin",
18187
+ name: "Check-in",
18188
+ description: "Take class attendance / check-in.",
18189
+ icon: "CheckCircle2",
18190
+ category: "management",
18191
+ permissions: ["management:write"],
18192
+ supportedRoles: ["teacher"],
18193
+ supportedDevices: ["desktop", "tablet", "mobile", "all"],
18194
+ tags: ["management", "attendance", "checkin"],
18195
+ version: "1.0.0",
18196
+ provider: OFFICIAL_PROVIDER,
18197
+ commandType: "attendance.record"
18198
+ },
18199
+ {
18200
+ id: "official_homework",
18201
+ name: "Homework",
18202
+ description: "Assign and track out-of-class homework.",
18203
+ icon: "BookOpen",
18204
+ category: "management",
18205
+ permissions: ["management:write"],
18206
+ supportedRoles: ["teacher", "student", "all"],
18207
+ supportedDevices: ["desktop", "tablet", "all"],
18208
+ tags: ["management", "homework", "assignment"],
18209
+ version: "1.0.0",
18210
+ provider: OFFICIAL_PROVIDER,
18211
+ commandType: "assignment.create"
18212
+ }
18213
+ ];
18214
+ function registerOfficialActivities(registry, actionRegistry) {
18215
+ for (const def of OFFICIAL_ACTIVITY_DEFINITIONS) {
18216
+ const descriptor = {
18217
+ ...def,
18218
+ aiAction: aiActionFor(def)
18219
+ };
18220
+ registry.registerProvider(new BaseActivityProvider({ descriptor }));
18221
+ if (actionRegistry && descriptor.aiAction) {
18222
+ actionRegistry.register(descriptor.aiAction);
18223
+ }
18224
+ }
18225
+ }
18226
+
18227
+ // packages/activity-ecosystem/index.ts
18228
+ var IActivityRegistryToken = new Token(
18229
+ "@openlearn/activity-ecosystem:IActivityRegistry"
18230
+ );
18231
+
18232
+ // server/routes/os.ts
18233
+ var import_path8 = __toESM(require("path"), 1);
18234
+ var import_fs8 = __toESM(require("fs"), 1);
18235
+ var import_child_process = require("child_process");
18236
+ var import_crypto7 = __toESM(require("crypto"), 1);
18237
+ var import_xss = require("xss");
18238
+
18239
+ // server/utils/crypto.ts
18240
+ var import_crypto5 = __toESM(require("crypto"), 1);
18241
+ var import_fs7 = __toESM(require("fs"), 1);
18242
+ var import_path6 = __toESM(require("path"), 1);
18243
+ var _encryptionKey = null;
18244
+ function getEncryptionKey() {
18245
+ if (_encryptionKey) return _encryptionKey;
18246
+ const envPath = import_path6.default.resolve(process.cwd(), ".env");
18247
+ const keyHex = process.env.ENCRYPTION_KEY;
18248
+ if (keyHex && keyHex.trim() !== "") {
18249
+ _encryptionKey = Buffer.from(keyHex.trim(), "hex");
18250
+ return _encryptionKey;
18251
+ }
18252
+ if (import_fs7.default.existsSync(envPath)) {
18253
+ try {
18254
+ const content = import_fs7.default.readFileSync(envPath, "utf-8");
18255
+ const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
18256
+ if (match && match[1].trim() !== "") {
18257
+ const fileKeyHex = match[1].trim();
18258
+ _encryptionKey = Buffer.from(fileKeyHex, "hex");
18259
+ process.env.ENCRYPTION_KEY = fileKeyHex;
18260
+ console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
18261
+ return _encryptionKey;
18262
+ }
18263
+ } catch {
18264
+ }
18265
+ }
18266
+ const newKey = import_crypto5.default.randomBytes(32).toString("hex");
18267
+ try {
18268
+ if (import_fs7.default.existsSync(envPath)) {
18269
+ let content = import_fs7.default.readFileSync(envPath, "utf-8");
18270
+ if (/^ENCRYPTION_KEY=/m.test(content)) {
18271
+ content = content.replace(/^ENCRYPTION_KEY=.*$/m, `ENCRYPTION_KEY=${newKey}`);
18272
+ import_fs7.default.writeFileSync(envPath, content);
18273
+ console.log("[Crypto] ENCRYPTION_KEY replaced in .env (was empty or invalid)");
18274
+ } else {
18275
+ import_fs7.default.appendFileSync(envPath, `
18276
+ ENCRYPTION_KEY=${newKey}
18277
+ `);
18278
+ console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
18279
+ }
18280
+ } else {
18281
+ import_fs7.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
18282
+ `);
18283
+ console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
18284
+ }
18285
+ } catch (e) {
18286
+ console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
18287
+ }
18288
+ process.env.ENCRYPTION_KEY = newKey;
18289
+ _encryptionKey = Buffer.from(newKey, "hex");
18290
+ return _encryptionKey;
18291
+ }
18292
+ function encryptApiKey(plaintext) {
18293
+ if (!plaintext) return "";
18294
+ const key = getEncryptionKey();
18295
+ const iv = import_crypto5.default.randomBytes(16);
18296
+ const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
18297
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
18298
+ const authTag = cipher.getAuthTag();
18299
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
18300
+ }
18301
+ function decryptApiKey(encrypted) {
18302
+ if (!encrypted) return "";
18303
+ const parts = encrypted.split(":");
18304
+ if (parts.length !== 3) return encrypted;
18305
+ try {
18306
+ const key = getEncryptionKey();
18307
+ const iv = Buffer.from(parts[0], "hex");
18308
+ const authTag = Buffer.from(parts[1], "hex");
18309
+ const ciphertext = Buffer.from(parts[2], "hex");
18310
+ const decipher = import_crypto5.default.createDecipheriv("aes-256-gcm", key, iv);
18311
+ decipher.setAuthTag(authTag);
18312
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
18313
+ } catch {
18314
+ console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
18315
+ return encrypted;
18316
+ }
18317
+ }
18318
+ function maskApiKey(key) {
18319
+ if (!key || key.length <= 8) return key ? "****" : "";
18320
+ return key.substring(0, 4) + "****" + key.substring(key.length - 4);
18321
+ }
18322
+ var PROMPT_INJECTION_PATTERNS = [
18323
+ /ignore\s+(all\s+)?(previous|prior|above|system)\s+(instructions?|prompts?|directives?)/i,
18324
+ /you\s+are\s+now\s+(a\s+)?(different|new|another)/i,
18325
+ /forget\s+(all\s+)?(your|the)\s+(training|instructions?|rules?)/i,
18326
+ /system\s*(prompt|message|instruction):\s*/i,
18327
+ /<\|im_start\|>/i,
18328
+ /<\|im_end\|>/i
18329
+ ];
18330
+ function detectPromptInjection(input) {
18331
+ return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
18332
+ }
18333
+
18334
+ // server/middleware/auth.ts
18335
+ function getValidSession(token) {
18336
+ let sessionRow;
18337
+ try {
18338
+ sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
18339
+ } catch {
18340
+ return null;
18341
+ }
18342
+ if (!sessionRow) return null;
18343
+ const now = Date.now();
18344
+ if (sessionRow.expires_at && sessionRow.expires_at < now) {
18345
+ kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18346
+ return null;
18347
+ }
18348
+ const idleTimeout = 24 * 60 * 60 * 1e3;
18349
+ if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
18350
+ kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18351
+ return null;
18352
+ }
18353
+ kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
18354
+ return JSON.parse(sessionRow.session_data);
18355
+ }
18356
+ function getCookieToken(req) {
18357
+ const rc = req.headers.cookie;
18358
+ if (!rc) return null;
18359
+ const parts = rc.split(";");
18360
+ for (const part of parts) {
18361
+ const trimmed = part.trim();
18362
+ if (trimmed.startsWith("edu_os_token=")) {
18363
+ return trimmed.substring("edu_os_token=".length);
18364
+ }
18365
+ }
18366
+ return null;
18367
+ }
18368
+ function getActorId(req) {
18369
+ const token = getCookieToken(req);
18370
+ if (!token) return "user-frontend";
18371
+ try {
18372
+ const session = getValidSession(token);
18373
+ if (!session) return "user-frontend";
18374
+ let role = session.subRole || session.role;
18375
+ if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
18376
+ role = "administrator";
18377
+ }
18378
+ if (role) {
18379
+ return `user:${session.userId || "demo"}:${role}`;
18380
+ }
18381
+ return "user-frontend";
18382
+ } catch {
18383
+ return "user-frontend";
18384
+ }
18385
+ }
18386
+ function checkIsTeacherOrAdmin(req) {
18387
+ const token = getCookieToken(req);
18388
+ if (!token) return false;
18389
+ try {
18390
+ const session = getValidSession(token);
18391
+ if (!session) return false;
18392
+ return session.role === "teacher" || session.role === "administrator";
18393
+ } catch {
18394
+ return false;
18395
+ }
18396
+ }
18397
+
18398
+ // server/routes/shared.ts
18399
+ var import_path7 = __toESM(require("path"), 1);
18400
+ var import_crypto6 = __toESM(require("crypto"), 1);
18401
+
18402
+ // server/utils/bridge-sdk.ts
18403
+ var BRIDGE_SDK_CODE = `(function() {
18404
+ // Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
18405
+ try {
18406
+ Object.defineProperty(document, 'cookie', {
18407
+ get: function() { return ""; },
18408
+ set: function(val) {},
18409
+ configurable: true
18410
+ });
18411
+ } catch (e) {
18412
+ try {
18413
+ Object.defineProperty(Document.prototype, 'cookie', {
18414
+ get: function() { return ""; },
18415
+ set: function(val) {},
18416
+ configurable: true
18417
+ });
18418
+ } catch (err) {}
18419
+ }
18420
+
18421
+ // \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
18422
+ function __makeSafePostMessage(realTarget, label) {
18423
+ return function(message, targetOrigin, transfer) {
18424
+ try {
18425
+ if (message && typeof message === 'object') {
18426
+ if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
18427
+ message.attempt_id = window.__LMS_STUDENT__.attempt_id;
18428
+ }
18429
+ if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
18430
+ message.uuid = window.__LMS_COURSEWARE__.uuid;
18431
+ }
18432
+ }
18433
+ } catch (e) {}
18434
+
18435
+ var origin = targetOrigin;
18436
+ if (origin === 'null' || origin === null || origin === undefined) {
18437
+ console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
18438
+ origin = '*';
18439
+ }
18440
+ try {
18441
+ return realTarget.postMessage.call(realTarget, message, origin, transfer);
18442
+ } catch (err) {
18443
+ if (err.name === 'SyntaxError' && origin !== '*') {
18444
+ console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
18445
+ return realTarget.postMessage.call(realTarget, message, '*', transfer);
18446
+ }
18447
+ throw err;
18448
+ }
18449
+ };
18450
+ }
18451
+
18452
+ // \u2500\u2500 Helper: create a Proxy wrapper around a cross-origin WindowProxy \u2500\u2500
18453
+ // This is necessary because sandboxed iframes (without allow-same-origin)
18454
+ // cannot set properties on cross-origin WindowProxy objects.
18455
+ // We use Object.defineProperty to shadow window.parent / window.top
18456
+ // with a Proxy that intercepts the .postMessage() call.
18457
+ function __proxyWindow(realRef, propName) {
18458
+ try {
18459
+ var safePost = __makeSafePostMessage(realRef, propName + '.postMessage');
18460
+ var proxyObj = new Proxy(realRef, {
18461
+ get: function(target, prop) {
18462
+ if (prop === 'postMessage') return safePost;
18463
+ try {
18464
+ var val = target[prop];
18465
+ if (typeof val === 'function') return val.bind(target);
18466
+ return val;
18467
+ } catch (e) { return undefined; }
18468
+ }
18469
+ });
18470
+ Object.defineProperty(window, propName, {
18471
+ get: function() { return proxyObj; },
18472
+ configurable: true
18473
+ });
18474
+ } catch (e) {
18475
+ // Proxy or defineProperty not supported, fall back to direct override attempt
18476
+ try { realRef.postMessage = __makeSafePostMessage(realRef, propName + '.postMessage (fallback)'); } catch (_) {}
18477
+ }
18478
+ }
18479
+
18480
+ // Proxy postMessage calls to enrich them with attempt_id/uuid and normalize targetOrigin
18481
+ try {
18482
+ // 1. Override window.postMessage (self-targeting, always works)
18483
+ var originalPostMessage = window.postMessage;
18484
+ window.postMessage = function(message, targetOrigin, transfer) {
18485
+ try {
18486
+ if (message && typeof message === 'object') {
18487
+ if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
18488
+ message.attempt_id = window.__LMS_STUDENT__.attempt_id;
18489
+ }
18490
+ if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
18491
+ message.uuid = window.__LMS_COURSEWARE__.uuid;
18492
+ }
18493
+ }
18494
+ } catch (e) {}
18495
+
18496
+ var origin = targetOrigin;
18497
+ if (origin === 'null' || origin === null || origin === undefined) {
18498
+ console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "null" to "*" for postMessage call');
18499
+ origin = '*';
18500
+ }
18501
+ try {
18502
+ return originalPostMessage.call(this, message, origin, transfer);
18503
+ } catch (err) {
18504
+ if (err.name === 'SyntaxError' && origin !== '*') {
18505
+ console.warn('[LMS Bridge Notice] Recovered SyntaxError on postMessage, fallback targetOrigin to "*"', err);
18506
+ return originalPostMessage.call(this, message, '*', transfer);
18507
+ }
18508
+ throw err;
18509
+ }
18510
+ };
18511
+
18512
+ // 2. Shadow window.parent with a Proxy that intercepts .postMessage()
18513
+ if (window.parent && window.parent !== window) {
18514
+ __proxyWindow(window.parent, 'parent');
18515
+ }
18516
+
18517
+ // 3. Shadow window.top with a Proxy that intercepts .postMessage()
18518
+ try {
18519
+ if (window.top && window.top !== window) {
18520
+ __proxyWindow(window.top, 'top');
18521
+ }
18522
+ } catch (e) {}
18523
+
18524
+ // 4. Intercept message event listeners to sanitize event.source.postMessage replies
18525
+ var origAddEventListener = window.addEventListener;
18526
+ if (typeof origAddEventListener === 'function') {
18527
+ window.addEventListener = function(type, listener, options) {
18528
+ if (type === 'message' && typeof listener === 'function') {
18529
+ var wrappedListener = function(event) {
18530
+ try {
18531
+ if (event && event.source && typeof event.source.postMessage === 'function') {
18532
+ var origSourcePostMessage = event.source.postMessage;
18533
+ event.source.postMessage = function(msg, targetOrigin, transfer) {
18534
+ var origin = targetOrigin;
18535
+ if (origin === 'null' || origin === null) {
18536
+ console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "null" to "*" on event.source.postMessage');
18537
+ origin = '*';
18538
+ }
18539
+ try {
18540
+ return origSourcePostMessage.call(event.source, msg, origin, transfer);
18541
+ } catch (err) {
18542
+ if (err.name === 'SyntaxError' && origin !== '*') {
18543
+ console.warn('[LMS Bridge Notice] Recovered SyntaxError on event.source.postMessage, fallback to "*"', err);
18544
+ return origSourcePostMessage.call(event.source, msg, '*', transfer);
18545
+ }
18546
+ throw err;
18547
+ }
18548
+ };
18549
+ }
18550
+ } catch (e) {}
18551
+ return listener.apply(this, arguments);
18552
+ };
18553
+ return origAddEventListener.call(this, type, wrappedListener, options);
18554
+ }
18555
+ return origAddEventListener.apply(this, arguments);
18556
+ };
18557
+ }
18558
+ } catch (e) {}
18559
+
18560
+ window.LMS = {
18561
+ submit(data) {
18562
+ window.parent.postMessage({
18563
+ type: "LMS_SUBMIT",
18564
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18565
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18566
+ payload: data
18567
+ }, "*");
18568
+ },
18569
+ saveProgress(data) {
18570
+ window.parent.postMessage({
18571
+ type: "LMS_SAVE_PROGRESS",
18572
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18573
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18574
+ payload: data
18575
+ }, "*");
18576
+ },
18577
+ finish(data) {
18578
+ window.parent.postMessage({
18579
+ type: "LMS_FINISH",
18580
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18581
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18582
+ payload: data
18583
+ }, "*");
18584
+ },
18585
+ getStudent() {
18586
+ return window.__LMS_STUDENT__;
18587
+ },
18588
+ getCourseware() {
18589
+ return window.__LMS_COURSEWARE__;
18590
+ },
18591
+ log(event, data) {
18592
+ window.parent.postMessage({
18593
+ type: "LMS_LOG",
18594
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18595
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18596
+ event: event,
18597
+ payload: data
18598
+ }, "*");
18599
+ }
18600
+ };
18601
+
18602
+ try {
18603
+ if (window.fetch) {
18604
+ const originalFetch = window.fetch;
18605
+ window.fetch = function(input, init) {
18606
+ try {
18607
+ const url = (typeof input === 'string') ? input : (input?.url || '');
18608
+ const method = init?.method || input?.method || 'GET';
18609
+ const headers = init?.headers || input?.headers || {};
18610
+ let body = init?.body || input?.body || null;
18611
+
18612
+ if (body && typeof body === 'object') {
18613
+ try { body = JSON.stringify(body); } catch(e){}
18614
+ }
18615
+
18616
+ if (url && !url.includes('/api/courseware/attempts/')) {
18617
+ window.parent.postMessage({
18618
+ type: "HOOK_FETCH",
18619
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18620
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18621
+ payload: { url, method, headers: JSON.parse(JSON.stringify(headers)), body: body ? body.toString() : null }
18622
+ }, "*");
18623
+ }
18624
+ } catch (e) {
18625
+ console.error("Bridge Hook fetch error", e);
18626
+ }
18627
+ return originalFetch.apply(this, arguments);
18628
+ };
18629
+ }
18630
+
18631
+ if (window.XMLHttpRequest) {
18632
+ const originalOpen = XMLHttpRequest.prototype.open;
18633
+ const originalSend = XMLHttpRequest.prototype.send;
18634
+ XMLHttpRequest.prototype.open = function(method, url) {
18635
+ this._method = method;
18636
+ this._url = url;
18637
+ return originalOpen.apply(this, arguments);
18638
+ };
18639
+ XMLHttpRequest.prototype.send = function(body) {
18640
+ try {
18641
+ let bodyStr = body;
18642
+ if (body && typeof body === 'object') {
18643
+ try { bodyStr = JSON.stringify(body); } catch(e){}
18644
+ }
18645
+ if (this._url && !this._url.includes('/api/courseware/attempts/')) {
18646
+ window.parent.postMessage({
18647
+ type: "HOOK_XHR",
18648
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18649
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18650
+ payload: { url: this._url, method: this._method, body: bodyStr ? bodyStr.toString() : null }
18651
+ }, "*");
18652
+ }
18653
+ } catch (e) {
18654
+ console.error("Bridge Hook XHR error", e);
18655
+ }
18656
+ return originalSend.apply(this, arguments);
18657
+ };
18658
+ }
18659
+
18660
+ function attachToAxios(axiosInstance) {
18661
+ if (axiosInstance && axiosInstance.interceptors && axiosInstance.interceptors.request) {
18662
+ axiosInstance.interceptors.request.use(function(config) {
18663
+ try {
18664
+ if (config.url && !config.url.includes('/api/courseware/attempts/')) {
18665
+ window.parent.postMessage({
18666
+ type: "HOOK_AXIOS",
18667
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18668
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18669
+ payload: { url: config.url, method: config.method, data: config.data }
18670
+ }, "*");
18671
+ }
18672
+ } catch (e) {
18673
+ console.error("Bridge Hook Axios error", e);
18674
+ }
18675
+ return config;
18676
+ }, function(error) { return Promise.reject(error); });
18677
+ }
18678
+ }
18679
+ if (window.axios) {
18680
+ attachToAxios(window.axios);
18681
+ }
18682
+ var _axios = window.axios;
18683
+ Object.defineProperty(window, 'axios', {
18684
+ get: function() { return _axios; },
18685
+ set: function(val) {
18686
+ _axios = val;
18687
+ attachToAxios(val);
18688
+ },
18689
+ configurable: true
18690
+ });
18691
+
18692
+ if (navigator && navigator.sendBeacon) {
18693
+ const originalSendBeacon = navigator.sendBeacon;
18694
+ navigator.sendBeacon = function(url, data) {
18695
+ try {
18696
+ if (url && !url.includes('/api/courseware/attempts/')) {
18697
+ window.parent.postMessage({
18698
+ type: "HOOK_BEACON",
18699
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18700
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18701
+ payload: { url: url, data: data ? data.toString() : null }
18702
+ }, "*");
18703
+ }
18704
+ } catch (e) {
18705
+ console.error("Bridge Hook Beacon error", e);
18706
+ }
18707
+ return originalSendBeacon.apply(this, arguments);
18708
+ };
18709
+ }
18710
+
18711
+ window.addEventListener('submit', function(e) {
18712
+ try {
18713
+ const form = e.target;
18714
+ const formData = new FormData(form);
18715
+ const data = {};
18716
+ formData.forEach(function(value, key) {
18717
+ data[key] = value;
18718
+ });
18719
+ if (form.action && !form.action.includes('/api/courseware/attempts/')) {
18720
+ window.parent.postMessage({
18721
+ type: "HOOK_FORM",
18722
+ uuid: window.__LMS_COURSEWARE__?.uuid,
18723
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
18724
+ payload: { action: form.action, method: form.method, data: data }
18725
+ }, "*");
18726
+ }
18727
+ } catch (err) {
18728
+ console.error("Bridge Hook Form error", err);
18729
+ }
18730
+ }, true);
18731
+
18732
+ // --- SMART DOM SCRAPER FOR GENERIC COURSEWARES ---
18733
+ function logToServer(msg, detail) {
18734
+ // \u4EC5 console \u8F93\u51FA\uFF1B\u7F51\u7EDC\u8BF7\u6C42\u53EF\u80FD\u88AB\u6D4F\u89C8\u5668 HTTPS \u5347\u7EA7\u5BFC\u81F4 ERR_CONNECTION_REFUSED
18735
+ try {
18736
+ console.log('[LMS Debug]', msg, detail || '');
18737
+ } catch (e) {}
18738
+ }
18739
+
18740
+ function findScoreInDOM() {
18741
+ const logData = [];
18742
+ try {
18743
+ const commonVars = ['score', 'points', 'grade', 'totalScore', 'currentScore', 'userScore', 'finalScore', 'correctCount'];
18744
+ for (const v of commonVars) {
18745
+ if (typeof window[v] === 'number') {
18746
+ logData.push("Global var " + v + " is number: " + window[v]);
18747
+ return { score: window[v], log: logData };
18748
+ }
18749
+ if (typeof window[v] === 'string') {
18750
+ const num = parseFloat(window[v]);
18751
+ if (!isNaN(num)) {
18752
+ logData.push("Global var " + v + " is string with number: " + window[v]);
18753
+ return { score: num, log: logData };
18754
+ }
18755
+ }
18756
+ }
18757
+
18758
+ const selectors = [
18759
+ '#score', '#scoreDisplay', '#score-num', '#scoreDisplaySpan', '#points', '#grade',
18760
+ '.score', '.points', '.grade', '.score-num', '.score-value',
18761
+ '[id*="score" i]', '[id*="point" i]', '[id*="grade" i]', '[id*="result" i]',
18762
+ '[class*="score" i]', '[class*="point" i]', '[class*="grade" i]', '[class*="result" i]'
18763
+ ];
18764
+
18765
+ for (const selector of selectors) {
18766
+ try {
18767
+ const el = document.querySelector(selector);
18768
+ if (el) {
18769
+ const text = (el.textContent || el.innerText || '').trim();
18770
+ if (text) {
18771
+ logData.push("Selector '" + selector + "' matched text: '" + text + "'");
18772
+ const fractionMatch = text.match(/(\\\\d+(\\\\.\\\\d+)?)\\\\s*[\\\\/|\u4E4B]\\\\s*(\\\\d+)/);
18773
+ if (fractionMatch) {
18774
+ const num = parseFloat(fractionMatch[1]);
18775
+ const den = parseFloat(fractionMatch[3]);
18776
+ if (den > 0) {
18777
+ const pct = (num / den) * 100;
18778
+ logData.push("Parsed fraction: " + num + "/" + den + " -> " + pct);
18779
+ return { score: pct, log: logData };
18780
+ }
18781
+ }
18782
+ const match = text.match(/\\\\d+(\\\\.\\\\d+)?/);
18783
+ if (match) {
18784
+ const num = parseFloat(match[0]);
18785
+ if (!isNaN(num)) {
18786
+ logData.push("Parsed decimal: " + num);
18787
+ return { score: num, log: logData };
18788
+ }
18789
+ }
18790
+ }
18791
+ }
18792
+ } catch (e) {}
18793
+ }
18794
+
18795
+ try {
18796
+ const inputs = document.querySelectorAll('input[type="text"], input[type="number"], input[readonly]');
18797
+ for (const input of inputs) {
18798
+ const id = (input.id || '').toLowerCase();
18799
+ const name = (input.name || '').toLowerCase();
18800
+ if (id.includes('score') || name.includes('score') || id.includes('point') || name.includes('point')) {
18801
+ const val = parseFloat(input.value);
18802
+ if (!isNaN(val)) {
18803
+ logData.push("Input id=" + id + " name=" + name + " value: " + input.value);
18804
+ return { score: val, log: logData };
18805
+ }
18806
+ }
18807
+ }
18808
+ } catch (e) {}
18809
+
18810
+ try {
18811
+ const all = document.getElementsByTagName('*');
18089
18812
  const ignoredTags = ['style', 'script', 'link', 'meta', 'svg', 'canvas', 'noscript', 'head', 'iframe'];
18090
18813
  for (let i = 0; i < all.length; i++) {
18091
18814
  const el = all[i];
@@ -18264,7 +18987,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
18264
18987
  const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
18265
18988
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, session.studentId, "active");
18266
18989
  if (!attempt) {
18267
- const attemptId = "att_" + import_crypto5.default.randomBytes(8).toString("hex");
18990
+ const attemptId = "att_" + import_crypto6.default.randomBytes(8).toString("hex");
18268
18991
  kernelContainer.db.prepare("INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)").run(attemptId, cwInfo.id, session.studentId, Date.now(), "active");
18269
18992
  attempt = { id: attemptId };
18270
18993
  }
@@ -18277,7 +19000,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
18277
19000
  } else if (session.role === "teacher" || session.role === "administrator") {
18278
19001
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
18279
19002
  if (!attempt) {
18280
- const attemptId = "att_teacher_" + import_crypto5.default.randomBytes(8).toString("hex");
19003
+ const attemptId = "att_teacher_" + import_crypto6.default.randomBytes(8).toString("hex");
18281
19004
  kernelContainer.db.prepare("INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)").run(attemptId, cwInfo.id, "teacher", Date.now(), "active");
18282
19005
  attempt = { id: attemptId };
18283
19006
  }
@@ -18293,7 +19016,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
18293
19016
  if (studentInfo.attempt_id === "guest-attempt") {
18294
19017
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
18295
19018
  if (!attempt) {
18296
- const attemptId = "att_guest_" + import_crypto5.default.randomBytes(8).toString("hex");
19019
+ const attemptId = "att_guest_" + import_crypto6.default.randomBytes(8).toString("hex");
18297
19020
  kernelContainer.db.prepare("INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)").run(attemptId, cwInfo.id, "guest", Date.now(), "active");
18298
19021
  attempt = { id: attemptId };
18299
19022
  }
@@ -18381,7 +19104,7 @@ function registerOsRoutes(ctx) {
18381
19104
  if (!import_fs8.default.existsSync(uploadsDir)) {
18382
19105
  import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
18383
19106
  }
18384
- const uniqueName = `${Date.now()}-${import_crypto6.default.randomBytes(4).toString("hex")}${ext}`;
19107
+ const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
18385
19108
  const filePath = import_path8.default.join(uploadsDir, uniqueName);
18386
19109
  import_fs8.default.writeFileSync(filePath, fileBuffer);
18387
19110
  let slideCount = 1;
@@ -18591,8 +19314,8 @@ function registerOsRoutes(ctx) {
18591
19314
  const result = provider ? await runOpenAIAgentChat2(provider, { message, lang, currentLessonId, attachments, callerRole, history }) : await runGeminiAgentChat2({ message, lang, currentLessonId, attachments, callerRole, history });
18592
19315
  if (result && typeof result.agentText === "string") {
18593
19316
  const now = Date.now();
18594
- kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + 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);
19317
+ kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto7.default.randomUUID(), convKey, "user", message, now);
19318
+ kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto7.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
18596
19319
  }
18597
19320
  res.json({
18598
19321
  success: true,
@@ -18934,7 +19657,7 @@ function registerResourcesRoutes(ctx) {
18934
19657
  // server/routes/courseware.ts
18935
19658
  var import_path9 = __toESM(require("path"), 1);
18936
19659
  var import_fs9 = __toESM(require("fs"), 1);
18937
- var import_crypto8 = __toESM(require("crypto"), 1);
19660
+ var import_crypto9 = __toESM(require("crypto"), 1);
18938
19661
  function registerCoursewareRoutes(ctx) {
18939
19662
  const {
18940
19663
  app,
@@ -19079,7 +19802,7 @@ function registerCoursewareRoutes(ctx) {
19079
19802
  try {
19080
19803
  const { attemptId } = req.params;
19081
19804
  const { eventType, payload } = req.body;
19082
- const rawId = "raw_" + import_crypto8.default.randomBytes(8).toString("hex");
19805
+ const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
19083
19806
  kernelContainer.db.prepare(
19084
19807
  "INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
19085
19808
  ).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
@@ -19107,7 +19830,7 @@ function registerCoursewareRoutes(ctx) {
19107
19830
  kernelContainer.db.prepare(
19108
19831
  "INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
19109
19832
  ).run(
19110
- "res_" + import_crypto8.default.randomBytes(8).toString("hex"),
19833
+ "res_" + import_crypto9.default.randomBytes(8).toString("hex"),
19111
19834
  attemptId,
19112
19835
  parsedScore,
19113
19836
  comment || null,
@@ -19258,7 +19981,7 @@ function registerCoursewareRoutes(ctx) {
19258
19981
  ).get(classId, lessonId, assignmentTitle);
19259
19982
  let assignmentId = assignment?.id;
19260
19983
  if (!assignmentId) {
19261
- assignmentId = "ast-cw-" + import_crypto8.default.randomBytes(8).toString("hex");
19984
+ assignmentId = "ast-cw-" + import_crypto9.default.randomBytes(8).toString("hex");
19262
19985
  kernelContainer.db.prepare(
19263
19986
  "INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
19264
19987
  ).run(
@@ -19415,7 +20138,7 @@ function registerBridgeRoutes(ctx) {
19415
20138
  }
19416
20139
 
19417
20140
  // server/routes/lessons.ts
19418
- var import_genai2 = require("@google/genai");
20141
+ var import_genai3 = require("@google/genai");
19419
20142
  function registerLessonsRoutes(ctx) {
19420
20143
  const {
19421
20144
  app,
@@ -19796,7 +20519,7 @@ function registerLessonsRoutes(ctx) {
19796
20519
  app.post("/api/lessons/:id/ai-tutor", async (req, res) => {
19797
20520
  try {
19798
20521
  const { elements } = req.body;
19799
- const ai = new import_genai2.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
20522
+ const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
19800
20523
  const elementsSummary = elements.map((e, i) => `Element ${i + 1}: type=${e.type}, content=${JSON.stringify(e.data)}`).join("\n");
19801
20524
  const prompt = `You are a real-time AI Tutor monitoring a student's interactive whiteboard.
19802
20525
  The student has pressed the "Ask AI" button for help.
@@ -20226,7 +20949,7 @@ function registerAdminRoutes(ctx) {
20226
20949
  // server/routes/roster.ts
20227
20950
  var import_path12 = __toESM(require("path"), 1);
20228
20951
  var import_fs11 = __toESM(require("fs"), 1);
20229
- var import_crypto9 = __toESM(require("crypto"), 1);
20952
+ var import_crypto10 = __toESM(require("crypto"), 1);
20230
20953
  var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
20231
20954
  function registerRosterRoutes(ctx) {
20232
20955
  const {
@@ -20486,7 +21209,7 @@ function registerRosterRoutes(ctx) {
20486
21209
  if (storedPwd.startsWith("$2")) {
20487
21210
  matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
20488
21211
  } else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
20489
- matches = import_crypto9.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
21212
+ matches = import_crypto10.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
20490
21213
  } else {
20491
21214
  matches = storedPwd === oldPassword;
20492
21215
  }
@@ -20558,7 +21281,7 @@ function registerRosterRoutes(ctx) {
20558
21281
  }
20559
21282
  const avatarDir = import_path12.default.join(process.cwd(), "uploads", "avatars");
20560
21283
  import_fs11.default.mkdirSync(avatarDir, { recursive: true });
20561
- const uniqueName = `${Date.now()}-${import_crypto9.default.randomBytes(4).toString("hex")}${ext}`;
21284
+ const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
20562
21285
  const filePath = import_path12.default.join(avatarDir, uniqueName);
20563
21286
  import_fs11.default.writeFileSync(filePath, fileBuffer);
20564
21287
  const avatarUrl = `/uploads/avatars/${uniqueName}`;
@@ -20677,7 +21400,7 @@ function registerRosterRoutes(ctx) {
20677
21400
  if (storedPwd.startsWith("$2")) {
20678
21401
  matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
20679
21402
  } else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
20680
- const sha256Hash = import_crypto9.default.createHash("sha256").update(providedPassword).digest("hex");
21403
+ const sha256Hash = import_crypto10.default.createHash("sha256").update(providedPassword).digest("hex");
20681
21404
  if (sha256Hash === storedPwd) {
20682
21405
  matchesOwnPassword = true;
20683
21406
  kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
@@ -20716,7 +21439,7 @@ function registerRosterRoutes(ctx) {
20716
21439
  };
20717
21440
  }
20718
21441
  if (sessionData) {
20719
- const sessionToken = "token_" + import_crypto9.default.randomBytes(16).toString("hex");
21442
+ const sessionToken = "token_" + import_crypto10.default.randomBytes(16).toString("hex");
20720
21443
  const now = Date.now();
20721
21444
  const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
20722
21445
  kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
@@ -21295,7 +22018,7 @@ function registerRosterRoutes(ctx) {
21295
22018
  }
21296
22019
 
21297
22020
  // server/routes/assignments.ts
21298
- var import_genai3 = require("@google/genai");
22021
+ var import_genai4 = require("@google/genai");
21299
22022
  function registerAssignmentsRoutes(ctx) {
21300
22023
  const {
21301
22024
  app,
@@ -21323,7 +22046,7 @@ function registerAssignmentsRoutes(ctx) {
21323
22046
  app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
21324
22047
  try {
21325
22048
  const { topic, lessonId } = req.body;
21326
- const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
22049
+ const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
21327
22050
  const prompt = `You are an expert teacher. Generate a short 1-question quiz or assignment about "${topic}". Output in this JSON format: {"title": "...", "description": "...", "content": "..."} without markdown blocks.`;
21328
22051
  const response = await ai.models.generateContent({ model: "gemini-3.5-flash", contents: prompt });
21329
22052
  const text = response.text || "{}";
@@ -21355,7 +22078,7 @@ function registerAssignmentsRoutes(ctx) {
21355
22078
  if (!lesson) {
21356
22079
  return res.status(404).json({ error: "Lesson not found" });
21357
22080
  }
21358
- const ai = new import_genai3.GoogleGenAI({
22081
+ const ai = new import_genai4.GoogleGenAI({
21359
22082
  apiKey: process.env.GEMINI_API_KEY,
21360
22083
  httpOptions: {
21361
22084
  headers: {
@@ -21380,26 +22103,26 @@ Generate the response in the specified JSON schema.`;
21380
22103
  config: {
21381
22104
  responseMimeType: "application/json",
21382
22105
  responseSchema: {
21383
- type: import_genai3.Type.OBJECT,
22106
+ type: import_genai4.Type.OBJECT,
21384
22107
  properties: {
21385
22108
  learningObjectives: {
21386
- type: import_genai3.Type.ARRAY,
21387
- items: { type: import_genai3.Type.STRING },
22109
+ type: import_genai4.Type.ARRAY,
22110
+ items: { type: import_genai4.Type.STRING },
21388
22111
  description: "List of identified key learning objectives for the lesson"
21389
22112
  },
21390
22113
  questions: {
21391
- type: import_genai3.Type.ARRAY,
22114
+ type: import_genai4.Type.ARRAY,
21392
22115
  items: {
21393
- type: import_genai3.Type.OBJECT,
22116
+ type: import_genai4.Type.OBJECT,
21394
22117
  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" },
22118
+ objective: { type: import_genai4.Type.STRING, description: "The specific learning objective tested by this question" },
22119
+ question: { type: import_genai4.Type.STRING, description: "The multiple-choice question text" },
21397
22120
  options: {
21398
- type: import_genai3.Type.ARRAY,
21399
- items: { type: import_genai3.Type.STRING },
22121
+ type: import_genai4.Type.ARRAY,
22122
+ items: { type: import_genai4.Type.STRING },
21400
22123
  description: "Exactly 4 options, including letter prefix like 'A) ...', 'B) ...'"
21401
22124
  },
21402
- correctAnswer: { type: import_genai3.Type.STRING, description: "The correct option (must exactly match one of the string options in the options array)" }
22125
+ correctAnswer: { type: import_genai4.Type.STRING, description: "The correct option (must exactly match one of the string options in the options array)" }
21403
22126
  },
21404
22127
  required: ["objective", "question", "options", "correctAnswer"]
21405
22128
  }
@@ -21471,7 +22194,7 @@ Generate the response in the specified JSON schema.`;
21471
22194
  const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
21472
22195
  const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
21473
22196
  if (!asb || !ast) throw new Error("Submission or assignment not found");
21474
- const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
22197
+ const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
21475
22198
  let grade = { score: 0, feedback: "" };
21476
22199
  let isMcqQuiz = false;
21477
22200
  let autoScore = null;
@@ -21554,7 +22277,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
21554
22277
  }
21555
22278
 
21556
22279
  // server/routes/schedules.ts
21557
- var import_genai4 = require("@google/genai");
22280
+ var import_genai5 = require("@google/genai");
21558
22281
  function registerSchedulesRoutes(ctx) {
21559
22282
  const {
21560
22283
  app,
@@ -21841,7 +22564,7 @@ function registerSchedulesRoutes(ctx) {
21841
22564
  console.warn(`[OCR Error] GEMINI_API_KEY is not configured`);
21842
22565
  return res.status(500).json({ error: lang === "zh" ? "\u672A\u914D\uFFFD? AI \u670D\u52A1\u3002\u8BF7\u5728\u7CFB\u7EDF\u8BBE\u7F6E\u4E2D\u6DFB\u52A0 AI Provider \u6216\u914D\uFFFD? GEMINI_API_KEY\uFFFD?" : "No AI provider configured. Please add an AI Provider in settings or set GEMINI_API_KEY." });
21843
22566
  }
21844
- const ai = new import_genai4.GoogleGenAI({ apiKey: geminiKey });
22567
+ const ai = new import_genai5.GoogleGenAI({ apiKey: geminiKey });
21845
22568
  const response = await ai.models.generateContent({
21846
22569
  model: "gemini-2.5-flash",
21847
22570
  contents: [{
@@ -21886,7 +22609,7 @@ function registerSchedulesRoutes(ctx) {
21886
22609
 
21887
22610
  // server/routes/grading.ts
21888
22611
  init_interfaces();
21889
- var import_genai5 = require("@google/genai");
22612
+ var import_genai6 = require("@google/genai");
21890
22613
  function registerGradingRoutes(ctx) {
21891
22614
  const {
21892
22615
  app,
@@ -22454,7 +23177,7 @@ ${examsText}
22454
23177
  if (!geminiKey) {
22455
23178
  return res.status(500).json({ error: "AI provider is not configured and GEMINI_API_KEY is missing." });
22456
23179
  }
22457
- const ai = new import_genai5.GoogleGenAI({ apiKey: geminiKey });
23180
+ const ai = new import_genai6.GoogleGenAI({ apiKey: geminiKey });
22458
23181
  const response = await ai.models.generateContent({
22459
23182
  model: "gemini-2.5-flash",
22460
23183
  contents: [{ role: "user", parts: [{ text: prompt }] }],
@@ -22809,798 +23532,411 @@ function registerPluginsRoutes(ctx) {
22809
23532
  const zipPath = import_path13.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
22810
23533
  if (import_fs12.default.existsSync(zipPath)) {
22811
23534
  zipBuffer = import_fs12.default.readFileSync(zipPath);
22812
- } else {
22813
- return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
22814
- }
22815
- }
22816
- const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
22817
- targetPluginId,
22818
- allowDowngrade: false
22819
- });
22820
- res.json({
22821
- success: true,
22822
- updated: true,
22823
- pluginId: result.pluginId,
22824
- manifest: result.manifest,
22825
- oldVersion: result.oldVersion,
22826
- newVersion: result.newVersion || "1.2.0",
22827
- wasActive: result.wasActive
22828
- });
22829
- } catch (err) {
22830
- console.error(err);
22831
- res.status(500).json({ success: false, error: err.message });
22832
- }
22833
- });
22834
- app.get("/api/plugins/:id(*)/contributions", (req, res) => {
22835
- try {
22836
- const rawId = decodeURIComponent(req.params.id);
22837
- const summary = kernelContainer.pluginHost.listContributions(rawId);
22838
- res.json({ success: true, result: summary });
22839
- } catch (e) {
22840
- res.status(500).json({ success: false, error: e.message });
22841
- }
22842
- });
22843
- app.get("/api/plugins/:id(*)/config", (req, res) => {
22844
- try {
22845
- const rawId = decodeURIComponent(req.params.id);
22846
- const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
22847
- const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
22848
- if (!row) {
22849
- return res.status(404).json({ success: false, error: "Plugin not found" });
22850
- }
22851
- const manifest = JSON.parse(row.manifest);
22852
- res.json({
22853
- success: true,
22854
- result: {
22855
- schema: manifest.configuration?.properties ?? {},
22856
- values: kernelContainer.pluginHost.getPluginConfig(pluginId, manifest)
22857
- }
22858
- });
22859
- } catch (e) {
22860
- res.status(500).json({ success: false, error: e.message });
22861
- }
22862
- });
22863
- app.post("/api/plugins/:id(*)/config", (req, res) => {
22864
- try {
22865
- const rawId = decodeURIComponent(req.params.id);
22866
- const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
22867
- const updates = req.body;
22868
- if (!updates || typeof updates !== "object") {
22869
- return res.status(400).json({ success: false, error: "Body must be an object of key-value pairs" });
22870
- }
22871
- const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
22872
- if (!row) {
22873
- return res.status(404).json({ success: false, error: "Plugin not found" });
22874
- }
22875
- const manifest = JSON.parse(row.manifest);
22876
- kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
22877
- res.json({ success: true });
22878
- } catch (e) {
22879
- res.status(500).json({ success: false, error: e.message });
22880
- }
22881
- });
22882
- app.post("/api/plugins/:id(*)/toggle", async (req, res) => {
22883
- try {
22884
- const rawId = decodeURIComponent(req.params.id);
22885
- const cmd = kernelContainer.commandBus.createCommand(
22886
- "plugin.toggle",
22887
- { pluginId: rawId },
22888
- getActorId(req)
22889
- );
22890
- const result = await kernelContainer.commandBus.execute(cmd);
22891
- res.json(result);
22892
- } catch (err) {
22893
- res.status(500).json({ success: false, error: err.message });
22894
- }
22895
- });
22896
- app.delete("/api/plugins/:id(*)", async (req, res) => {
22897
- try {
22898
- const rawId = decodeURIComponent(req.params.id);
22899
- const cmd = kernelContainer.commandBus.createCommand(
22900
- "plugin.uninstall",
22901
- { pluginId: rawId },
22902
- getActorId(req)
22903
- );
22904
- const result = await kernelContainer.commandBus.execute(cmd);
22905
- res.json(result);
22906
- } catch (err) {
22907
- res.status(500).json({ success: false, error: err.message });
22908
- }
22909
- });
22910
- app.get("/api/plugins/:id(*)", async (req, res) => {
22911
- try {
22912
- const rawId = decodeURIComponent(req.params.id);
22913
- const cmd = kernelContainer.commandBus.createCommand(
22914
- "plugin.info",
22915
- { pluginId: rawId },
22916
- getActorId(req)
22917
- );
22918
- const result = await kernelContainer.commandBus.execute(cmd);
22919
- res.json(result);
22920
- } catch (err) {
22921
- res.status(404).json({ success: false, error: err.message });
22922
- }
22923
- });
22924
- app.post("/api/plugins", async (req, res) => {
22925
- try {
22926
- const { sourceCode } = req.body;
22927
- const cmd = kernelContainer.commandBus.createCommand(
22928
- "plugin.install",
22929
- { sourceCode },
22930
- getActorId(req)
22931
- );
22932
- const result = await kernelContainer.commandBus.execute(cmd);
22933
- res.json(result);
22934
- } catch (err) {
22935
- console.error(err);
22936
- res.status(500).json({ success: false, error: err.message });
22937
- }
22938
- });
22939
- app.post("/api/plugins/upload-zip", async (req, res) => {
22940
- try {
22941
- const { base64Data, filename, executionMode } = req.body;
22942
- const cmd = kernelContainer.commandBus.createCommand(
22943
- "plugin.install_zip",
22944
- { base64Data, filename, executionMode },
22945
- getActorId(req)
22946
- );
22947
- const result = await kernelContainer.commandBus.execute(cmd);
22948
- res.json(result);
22949
- } catch (err) {
22950
- console.error(err);
22951
- res.status(500).json({ success: false, error: err.message });
22952
- }
22953
- });
22954
- app.post("/api/plugins/upload-zip-raw", import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
22955
- try {
22956
- const zipBuffer = req.body;
22957
- const filename = req.headers["x-filename"] ? decodeURIComponent(req.headers["x-filename"]) : "plugin.zip";
22958
- const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
22959
- const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
22960
- const modeHeader = String(req.headers["x-install-mode"] || "install").toLowerCase();
22961
- const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
22962
- const targetPluginId = req.headers["x-target-plugin-id"] ? decodeURIComponent(String(req.headers["x-target-plugin-id"])) : void 0;
22963
- if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
22964
- return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
22965
- }
22966
- if (modeHeader === "update") {
22967
- const result2 = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
22968
- targetPluginId,
22969
- executionMode,
22970
- allowDowngrade
22971
- });
22972
- return res.json({
22973
- success: true,
22974
- updated: true,
22975
- pluginId: result2.pluginId,
22976
- manifest: result2.manifest,
22977
- oldVersion: result2.oldVersion,
22978
- newVersion: result2.newVersion,
22979
- wasActive: result2.wasActive,
22980
- filename
22981
- });
22982
- }
22983
- const result = await kernelContainer.pluginDistributionManager.installFromZip(zipBuffer, executionMode);
22984
- res.json({
22985
- success: true,
22986
- updated: false,
22987
- pluginId: result.pluginId,
22988
- manifest: result.manifest,
22989
- filename
22990
- });
22991
- } catch (err) {
22992
- console.error(err);
22993
- res.status(500).json({ success: false, error: err.message });
22994
- }
22995
- });
22996
- app.post(
22997
- "/api/plugins/:id(*)/update-zip-raw",
22998
- import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }),
22999
- async (req, res) => {
23000
- try {
23001
- const targetPluginId = decodeURIComponent(req.params.id);
23002
- const zipBuffer = req.body;
23003
- const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
23004
- const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
23005
- const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
23006
- if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
23007
- return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
23008
- }
23009
- const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
23010
- targetPluginId,
23011
- executionMode,
23012
- allowDowngrade
23013
- });
23014
- res.json({
23015
- success: true,
23016
- updated: true,
23017
- pluginId: result.pluginId,
23018
- manifest: result.manifest,
23019
- oldVersion: result.oldVersion,
23020
- newVersion: result.newVersion,
23021
- wasActive: result.wasActive
23022
- });
23023
- } catch (err) {
23024
- console.error(err);
23025
- res.status(500).json({ success: false, error: err.message });
23026
- }
23027
- }
23028
- );
23029
- app.post("/api/plugins/execute-command", async (req, res) => {
23030
- try {
23031
- const { type, payload } = req.body;
23032
- if (!type) {
23033
- return res.status(400).json({ success: false, error: "Missing command type" });
23034
- }
23035
- let resolvedType = type;
23036
- const bus = kernelContainer.commandBus;
23037
- const handlersMap = bus.handlers;
23038
- const legacyMap = bus.legacyHandlers;
23039
- if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
23040
- for (const map of [handlersMap, legacyMap]) {
23041
- if (!map) continue;
23042
- for (const [key] of map) {
23043
- if (key.endsWith(":" + resolvedType) || key.endsWith("." + resolvedType)) {
23044
- resolvedType = key;
23045
- break;
23046
- }
23047
- }
23048
- if (resolvedType !== type) break;
23049
- }
23050
- }
23051
- if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
23052
- console.error("[execute-command] Handler NOT FOUND for type:", resolvedType);
23053
- console.error(
23054
- "[execute-command] Registered handlers:",
23055
- [...handlersMap?.keys?.() ?? []].join(", ") || "(none)"
23056
- );
23057
- const matching = [...handlersMap?.keys?.() ?? []].filter((k) => k.includes("courseware"));
23058
- console.error("[execute-command] Matching courseware keys:", matching.join(", ") || "(none)");
23059
- }
23060
- const cmd = await kernelContainer.commandBus.createCommand(
23061
- resolvedType,
23062
- payload ?? {},
23063
- getActorId(req)
23064
- );
23065
- const result = await kernelContainer.commandBus.execute(cmd);
23066
- res.json({ success: true, result });
23535
+ } else {
23536
+ return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
23537
+ }
23538
+ }
23539
+ const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
23540
+ targetPluginId,
23541
+ allowDowngrade: false
23542
+ });
23543
+ res.json({
23544
+ success: true,
23545
+ updated: true,
23546
+ pluginId: result.pluginId,
23547
+ manifest: result.manifest,
23548
+ oldVersion: result.oldVersion,
23549
+ newVersion: result.newVersion || "1.2.0",
23550
+ wasActive: result.wasActive
23551
+ });
23067
23552
  } catch (err) {
23068
- console.error("[execute-command]", err.message);
23553
+ console.error(err);
23069
23554
  res.status(500).json({ success: false, error: err.message });
23070
23555
  }
23071
23556
  });
23072
- app.get("/api/ai-providers", (req, res) => {
23557
+ app.get("/api/plugins/:id(*)/contributions", (req, res) => {
23073
23558
  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);
23559
+ const rawId = decodeURIComponent(req.params.id);
23560
+ const summary = kernelContainer.pluginHost.listContributions(rawId);
23561
+ res.json({ success: true, result: summary });
23080
23562
  } catch (e) {
23081
- res.status(500).json({ error: e.message });
23563
+ res.status(500).json({ success: false, error: e.message });
23082
23564
  }
23083
23565
  });
23084
- app.post("/api/ai-providers", (req, res) => {
23566
+ app.get("/api/plugins/:id(*)/config", (req, res) => {
23085
23567
  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" });
23568
+ const rawId = decodeURIComponent(req.params.id);
23569
+ const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
23570
+ const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
23571
+ if (!row) {
23572
+ return res.status(404).json({ success: false, error: "Plugin not found" });
23089
23573
  }
23090
- const 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 });
23574
+ const manifest = JSON.parse(row.manifest);
23575
+ res.json({
23576
+ success: true,
23577
+ result: {
23578
+ schema: manifest.configuration?.properties ?? {},
23579
+ values: kernelContainer.pluginHost.getPluginConfig(pluginId, manifest)
23580
+ }
23581
+ });
23095
23582
  } catch (e) {
23096
- res.status(500).json({ error: e.message });
23583
+ res.status(500).json({ success: false, error: e.message });
23097
23584
  }
23098
23585
  });
23099
- app.put("/api/ai-providers/:id", (req, res) => {
23586
+ app.post("/api/plugins/:id(*)/config", (req, res) => {
23100
23587
  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" });
23588
+ const rawId = decodeURIComponent(req.params.id);
23589
+ const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
23590
+ const updates = req.body;
23591
+ if (!updates || typeof updates !== "object") {
23592
+ return res.status(400).json({ success: false, error: "Body must be an object of key-value pairs" });
23104
23593
  }
23105
- const 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 || "";
23594
+ const row = kernelContainer.db.prepare("SELECT manifest FROM plugins WHERE id = ?").get(pluginId);
23595
+ if (!row) {
23596
+ return res.status(404).json({ success: false, error: "Plugin not found" });
23112
23597
  }
23113
- 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);
23598
+ const manifest = JSON.parse(row.manifest);
23599
+ kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
23114
23600
  res.json({ success: true });
23115
23601
  } catch (e) {
23116
- res.status(500).json({ error: e.message });
23602
+ res.status(500).json({ success: false, error: e.message });
23117
23603
  }
23118
23604
  });
23119
- app.delete("/api/ai-providers/:id", (req, res) => {
23605
+ app.post("/api/plugins/:id(*)/toggle", async (req, res) => {
23120
23606
  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 });
23607
+ const rawId = decodeURIComponent(req.params.id);
23608
+ const cmd = kernelContainer.commandBus.createCommand(
23609
+ "plugin.toggle",
23610
+ { pluginId: rawId },
23611
+ getActorId(req)
23612
+ );
23613
+ const result = await kernelContainer.commandBus.execute(cmd);
23614
+ res.json(result);
23615
+ } catch (err) {
23616
+ res.status(500).json({ success: false, error: err.message });
23125
23617
  }
23126
23618
  });
23127
- app.get("/api/site-settings", (req, res) => {
23619
+ app.delete("/api/plugins/:id(*)", async (req, res) => {
23128
23620
  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 });
23621
+ const rawId = decodeURIComponent(req.params.id);
23622
+ const cmd = kernelContainer.commandBus.createCommand(
23623
+ "plugin.uninstall",
23624
+ { pluginId: rawId },
23625
+ getActorId(req)
23626
+ );
23627
+ const result = await kernelContainer.commandBus.execute(cmd);
23628
+ res.json(result);
23629
+ } catch (err) {
23630
+ res.status(500).json({ success: false, error: err.message });
23137
23631
  }
23138
23632
  });
23139
- app.put("/api/site-settings", (req, res) => {
23633
+ app.get("/api/plugins/:id(*)", async (req, res) => {
23140
23634
  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 });
23635
+ const rawId = decodeURIComponent(req.params.id);
23636
+ const cmd = kernelContainer.commandBus.createCommand(
23637
+ "plugin.info",
23638
+ { pluginId: rawId },
23639
+ getActorId(req)
23640
+ );
23641
+ const result = await kernelContainer.commandBus.execute(cmd);
23642
+ res.json(result);
23643
+ } catch (err) {
23644
+ res.status(404).json({ success: false, error: err.message });
23152
23645
  }
23153
23646
  });
23154
- app.post("/api/ai-providers/test", async (req, res) => {
23647
+ app.post("/api/plugins", async (req, res) => {
23155
23648
  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)])
23649
+ const { sourceCode } = req.body;
23650
+ const cmd = kernelContainer.commandBus.createCommand(
23651
+ "plugin.install",
23652
+ { sourceCode },
23653
+ getActorId(req)
23265
23654
  );
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)
23655
+ const result = await kernelContainer.commandBus.execute(cmd);
23656
+ res.json(result);
23657
+ } catch (err) {
23658
+ console.error(err);
23659
+ res.status(500).json({ success: false, error: err.message });
23284
23660
  }
23285
- }));
23286
- };
23287
- var executeAgentToolCall = async (toolName, args, allExecutedTools, callerRole, currentLessonId) => {
23288
- const actionDesc = kernelContainer.actionRegistry.getActionByToolName(toolName);
23289
- let actionResult;
23290
- const isAdmin = callerRole === "administrator";
23291
- const actorId = isAdmin ? "user-frontend" : "agent-system-0";
23292
- const metadata = isAdmin ? { approved: true } : void 0;
23293
- if (actionDesc) {
23294
- const cmd = kernelContainer.commandBus.createCommand(
23295
- actionDesc.commandType,
23296
- args,
23297
- actorId,
23298
- metadata
23299
- );
23661
+ });
23662
+ app.post("/api/plugins/upload-zip", async (req, res) => {
23300
23663
  try {
23301
- const 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
- }
23664
+ const { base64Data, filename, executionMode } = req.body;
23665
+ const cmd = kernelContainer.commandBus.createCommand(
23666
+ "plugin.install_zip",
23667
+ { base64Data, filename, executionMode },
23668
+ getActorId(req)
23669
+ );
23670
+ const result = await kernelContainer.commandBus.execute(cmd);
23671
+ res.json(result);
23330
23672
  } catch (err) {
23331
- actionResult = { error: err.message };
23332
- allExecutedTools.push({ callName: toolName, success: false, error: err.message });
23673
+ console.error(err);
23674
+ res.status(500).json({ success: false, error: err.message });
23333
23675
  }
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
23676
+ });
23677
+ app.post("/api/plugins/upload-zip-raw", import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
23678
+ try {
23679
+ const zipBuffer = req.body;
23680
+ const filename = req.headers["x-filename"] ? decodeURIComponent(req.headers["x-filename"]) : "plugin.zip";
23681
+ const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
23682
+ const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
23683
+ const modeHeader = String(req.headers["x-install-mode"] || "install").toLowerCase();
23684
+ const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
23685
+ const targetPluginId = req.headers["x-target-plugin-id"] ? decodeURIComponent(String(req.headers["x-target-plugin-id"])) : void 0;
23686
+ if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
23687
+ return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
23376
23688
  }
23377
- });
23378
- 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
- }
23689
+ if (modeHeader === "update") {
23690
+ const result2 = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
23691
+ targetPluginId,
23692
+ executionMode,
23693
+ allowDowngrade
23694
+ });
23695
+ return res.json({
23696
+ success: true,
23697
+ updated: true,
23698
+ pluginId: result2.pluginId,
23699
+ manifest: result2.manifest,
23700
+ oldVersion: result2.oldVersion,
23701
+ newVersion: result2.newVersion,
23702
+ wasActive: result2.wasActive,
23703
+ filename
23410
23704
  });
23411
23705
  }
23706
+ const result = await kernelContainer.pluginDistributionManager.installFromZip(zipBuffer, executionMode);
23707
+ res.json({
23708
+ success: true,
23709
+ updated: false,
23710
+ pluginId: result.pluginId,
23711
+ manifest: result.manifest,
23712
+ filename
23713
+ });
23714
+ } catch (err) {
23715
+ console.error(err);
23716
+ res.status(500).json({ success: false, error: err.message });
23412
23717
  }
23413
- 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 = {};
23718
+ });
23719
+ app.post(
23720
+ "/api/plugins/:id(*)/update-zip-raw",
23721
+ import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }),
23722
+ async (req, res) => {
23723
+ try {
23724
+ const targetPluginId = decodeURIComponent(req.params.id);
23725
+ const zipBuffer = req.body;
23726
+ const executionModeHeader = String(req.headers["x-execution-mode"] || "").toLowerCase();
23727
+ const executionMode = executionModeHeader === "worker" || executionModeHeader === "inline" ? executionModeHeader : void 0;
23728
+ const allowDowngrade = String(req.headers["x-allow-downgrade"] || "").toLowerCase() === "true";
23729
+ if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
23730
+ return res.status(400).json({ success: false, error: "Empty or invalid zip file" });
23489
23731
  }
23732
+ const result = await kernelContainer.pluginDistributionManager.updateFromZip(zipBuffer, {
23733
+ targetPluginId,
23734
+ executionMode,
23735
+ allowDowngrade
23736
+ });
23737
+ res.json({
23738
+ success: true,
23739
+ updated: true,
23740
+ pluginId: result.pluginId,
23741
+ manifest: result.manifest,
23742
+ oldVersion: result.oldVersion,
23743
+ newVersion: result.newVersion,
23744
+ wasActive: result.wasActive
23745
+ });
23746
+ } catch (err) {
23747
+ console.error(err);
23748
+ res.status(500).json({ success: false, error: err.message });
23490
23749
  }
23491
- 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;
23750
+ }
23751
+ );
23752
+ app.post("/api/plugins/execute-command", async (req, res) => {
23753
+ try {
23754
+ const { type, payload } = req.body;
23755
+ if (!type) {
23756
+ return res.status(400).json({ success: false, error: "Missing command type" });
23757
+ }
23758
+ let resolvedType = type;
23759
+ const bus = kernelContainer.commandBus;
23760
+ const handlersMap = bus.handlers;
23761
+ const legacyMap = bus.legacyHandlers;
23762
+ if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
23763
+ for (const map of [handlersMap, legacyMap]) {
23764
+ if (!map) continue;
23765
+ for (const [key] of map) {
23766
+ if (key.endsWith(":" + resolvedType) || key.endsWith("." + resolvedType)) {
23767
+ resolvedType = key;
23768
+ break;
23498
23769
  }
23499
23770
  }
23771
+ if (resolvedType !== type) break;
23500
23772
  }
23501
23773
  }
23502
- 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
- });
23774
+ if (!handlersMap?.has?.(resolvedType) && !legacyMap?.has?.(resolvedType)) {
23775
+ console.error("[execute-command] Handler NOT FOUND for type:", resolvedType);
23776
+ console.error(
23777
+ "[execute-command] Registered handlers:",
23778
+ [...handlersMap?.keys?.() ?? []].join(", ") || "(none)"
23779
+ );
23780
+ const matching = [...handlersMap?.keys?.() ?? []].filter((k) => k.includes("courseware"));
23781
+ console.error("[execute-command] Matching courseware keys:", matching.join(", ") || "(none)");
23782
+ }
23783
+ const cmd = await kernelContainer.commandBus.createCommand(
23784
+ resolvedType,
23785
+ payload ?? {},
23786
+ getActorId(req)
23787
+ );
23788
+ const result = await kernelContainer.commandBus.execute(cmd);
23789
+ res.json({ success: true, result });
23790
+ } catch (err) {
23791
+ console.error("[execute-command]", err.message);
23792
+ res.status(500).json({ success: false, error: err.message });
23508
23793
  }
23509
- loopCount++;
23510
- }
23511
- if (loopCount >= MAX_LOOPS && !finalResponseText) {
23512
- finalResponseText = "I have executed several internal commands, but reached the iteration limit. Please review the assistant panel for the latest state.";
23513
- }
23514
- return {
23515
- agentText: finalResponseText,
23516
- toolResults: allExecutedTools
23517
- };
23518
- };
23519
- async function startServer() {
23520
- await ServerBootstrapAdapter.bootstrap({
23521
- kernelContainer,
23522
- environment: process.env.NODE_ENV || "development",
23523
- config: { port: Number(process.env.PORT) || 9e3 }
23524
23794
  });
23525
- 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);
23795
+ app.get("/api/ai-providers", (req, res) => {
23796
+ try {
23797
+ const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
23798
+ const masked = providers.map((p) => ({
23799
+ ...p,
23800
+ api_key: maskApiKey(decryptApiKey(p.api_key || ""))
23801
+ }));
23802
+ res.json(masked);
23803
+ } catch (e) {
23804
+ res.status(500).json({ error: e.message });
23530
23805
  }
23531
- 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);
23806
+ });
23807
+ app.post("/api/ai-providers", (req, res) => {
23808
+ try {
23809
+ const { name, api_url, api_key, model_name } = req.body;
23810
+ if (!name || !api_url || !model_name) {
23811
+ return res.status(400).json({ error: "Missing name, api_url or model_name" });
23812
+ }
23813
+ const id = "prov_" + Date.now();
23814
+ const now = Date.now();
23815
+ const encryptedKey = api_key ? encryptApiKey(api_key) : "";
23816
+ kernelContainer.db.prepare("INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, name, api_url, encryptedKey, model_name, now, now);
23817
+ res.json({ success: true, id });
23818
+ } catch (e) {
23819
+ res.status(500).json({ error: e.message });
23535
23820
  }
23536
- } 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.`);
23821
+ });
23822
+ app.put("/api/ai-providers/:id", (req, res) => {
23823
+ try {
23824
+ const { name, api_url, api_key, model_name } = req.body;
23825
+ if (!name || !api_url || !model_name) {
23826
+ return res.status(400).json({ error: "Missing name, api_url or model_name" });
23827
+ }
23828
+ const now = Date.now();
23829
+ let finalKey;
23830
+ if (api_key && api_key.trim() !== "" && !api_key.includes("****")) {
23831
+ finalKey = encryptApiKey(api_key);
23832
+ } else {
23833
+ const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ?").get(req.params.id);
23834
+ finalKey = existing?.api_key || "";
23835
+ }
23836
+ kernelContainer.db.prepare("UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?").run(name, api_url, finalKey, model_name, now, req.params.id);
23837
+ res.json({ success: true });
23838
+ } catch (e) {
23839
+ res.status(500).json({ error: e.message });
23840
+ }
23841
+ });
23842
+ app.delete("/api/ai-providers/:id", (req, res) => {
23843
+ try {
23844
+ kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
23845
+ res.json({ success: true });
23846
+ } catch (e) {
23847
+ res.status(500).json({ error: e.message });
23848
+ }
23849
+ });
23850
+ app.get("/api/site-settings", (req, res) => {
23851
+ try {
23852
+ const row = kernelContainer.db.prepare("SELECT site_name, slogan, logo_url FROM site_settings WHERE id = ?").get("global");
23853
+ res.json({
23854
+ siteName: row?.site_name || "",
23855
+ slogan: row?.slogan || "",
23856
+ logoUrl: row?.logo_url || null
23857
+ });
23858
+ } catch (e) {
23859
+ res.status(500).json({ error: e.message });
23860
+ }
23861
+ });
23862
+ app.put("/api/site-settings", (req, res) => {
23863
+ try {
23864
+ const { siteName, slogan, logoUrl } = req.body || {};
23865
+ kernelContainer.db.prepare(
23866
+ `INSERT INTO site_settings (id, site_name, slogan, logo_url) VALUES ('global', ?, ?, ?)
23867
+ ON CONFLICT(id) DO UPDATE SET site_name = excluded.site_name, slogan = excluded.slogan, logo_url = excluded.logo_url`
23868
+ ).run(siteName || "", slogan || "", logoUrl || null);
23869
+ res.json({
23870
+ success: true,
23871
+ siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
23872
+ });
23873
+ } catch (e) {
23874
+ res.status(500).json({ error: e.message });
23875
+ }
23876
+ });
23877
+ app.post("/api/ai-providers/test", async (req, res) => {
23878
+ try {
23879
+ const { api_url, api_key: providedKey, model_name } = req.body;
23880
+ if (!api_url || !model_name) {
23881
+ return res.status(400).json({ error: "api_url and model_name are required" });
23882
+ }
23883
+ let api_key = "";
23884
+ if (providedKey && providedKey.includes("****")) {
23885
+ const existing = kernelContainer.db.prepare(
23886
+ "SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? LIMIT 1"
23887
+ ).get(api_url, model_name);
23888
+ api_key = existing ? decryptApiKey(existing.api_key) : "";
23889
+ } else if (providedKey) {
23890
+ api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
23891
+ }
23892
+ const controller = new AbortController();
23893
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
23894
+ let cleanUrl = api_url.trim();
23895
+ if (!cleanUrl.endsWith("/chat/completions")) {
23896
+ cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
23897
+ }
23898
+ const response = await fetch(cleanUrl, {
23899
+ method: "POST",
23900
+ headers: {
23901
+ "Content-Type": "application/json",
23902
+ "Authorization": `Bearer ${api_key || ""}`
23903
+ },
23904
+ body: JSON.stringify({
23905
+ model: model_name,
23906
+ messages: [{ role: "user", content: "Say connected" }],
23907
+ max_tokens: 5
23908
+ }),
23909
+ signal: controller.signal
23910
+ });
23911
+ clearTimeout(timeoutId);
23912
+ const responseText = await response.text();
23913
+ if (response.ok) {
23914
+ res.json({ success: true, message: "Successfully connected and received response." });
23915
+ } else {
23916
+ res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
23917
+ }
23918
+ } catch (e) {
23919
+ res.status(500).json({ success: false, error: `Connection failed: ${e.message}` });
23600
23920
  }
23601
- } catch (e) {
23602
- console.warn("[Session] Could not clean up expired sessions:", e);
23921
+ });
23922
+ }
23923
+
23924
+ // server.ts
23925
+ import_dotenv.default.config();
23926
+ if (!process.env.NODE_ENV) {
23927
+ const isCjs = typeof __filename !== "undefined" && __filename.endsWith(".cjs");
23928
+ const isDist = process.cwd().endsWith("/dist") || typeof __dirname !== "undefined" && __dirname.includes("/dist") || typeof __filename !== "undefined" && __filename.includes("/dist");
23929
+ if (isCjs || isDist) {
23930
+ process.env.NODE_ENV = "production";
23603
23931
  }
23932
+ }
23933
+ async function startServer() {
23934
+ await ServerBootstrapAdapter.bootstrap({
23935
+ kernelContainer,
23936
+ environment: process.env.NODE_ENV || "development",
23937
+ config: { port: Number(process.env.PORT) || 9e3 }
23938
+ });
23939
+ await runStartupMigrations(kernelContainer.db);
23604
23940
  await kernelContainer.ready;
23605
23941
  const activityRegistry = new ActivityRegistry2();
23606
23942
  registerOfficialActivities(activityRegistry, kernelContainer.actionRegistry);
@@ -23619,7 +23955,7 @@ async function startServer() {
23619
23955
  imgSrc: ["'self'", "data:", "blob:", "https:"],
23620
23956
  connectSrc: ["'self'", "ws:", "wss:", "https:"],
23621
23957
  fontSrc: ["'self'", "data:", "https:", "https://fonts.gstatic.com"],
23622
- frameSrc: ["'self'"],
23958
+ frameSrc: ["'self'", "blob:", "data:", "http://localhost", "http://127.0.0.1", "http:", "https:"],
23623
23959
  objectSrc: ["'none'"]
23624
23960
  }
23625
23961
  },
@@ -23695,230 +24031,8 @@ async function startServer() {
23695
24031
  registerSchedulesRoutes(ctx);
23696
24032
  registerGradingRoutes(ctx);
23697
24033
  registerPluginsRoutes(ctx);
23698
- kernelContainer.eventBus.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
- });
24034
+ setupRealtimeBridge({ eventBus: kernelContainer.eventBus, io, db: kernelContainer.db });
24035
+ setupPresence({ io, eventBus: kernelContainer.eventBus });
23922
24036
  if (process.env.NODE_ENV !== "production") {
23923
24037
  const vite = await (0, import_vite.createServer)({
23924
24038
  server: {