openlearn-next 0.3.6 → 0.3.8

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.
Files changed (33) hide show
  1. package/cli-cleaner.mjs +131 -0
  2. package/cli-data.mjs +252 -0
  3. package/cli-doctor.mjs +189 -0
  4. package/cli.mjs +186 -16
  5. package/dist/assets/{BatchPickerModal-C98sjlMF.js → BatchPickerModal-yr3TOeuu.js} +1 -1
  6. package/dist/assets/{ClassesView-BPR46qdP.js → ClassesView-CJBu14nu.js} +1 -1
  7. package/dist/assets/{CloudDriveModal-muU449iB.js → CloudDriveModal-BYPpuwx4.js} +1 -1
  8. package/dist/assets/{CourseManagement-BRucWTbo.js → CourseManagement-D7HitLiU.js} +1 -1
  9. package/dist/assets/{CourseWizardModal-DTSK7VGp.js → CourseWizardModal-CyMoK1Os.js} +1 -1
  10. package/dist/assets/{Dashboard-COtcZSPW.js → Dashboard-DjCWV3Qg.js} +1 -1
  11. package/dist/assets/{ExportWeightModal-Dd7qjZuJ.js → ExportWeightModal-B5Cj-pLX.js} +1 -1
  12. package/dist/assets/{HelpView-CCmQYk87.js → HelpView-CxTV4J6V.js} +1 -1
  13. package/dist/assets/{ImportLessonsModal-cNlJ5wwh.js → ImportLessonsModal-CtHd6jIE.js} +1 -1
  14. package/dist/assets/{InteractiveCoursewareViewer-Cok6OVNk.js → InteractiveCoursewareViewer-CiOJP-Y5.js} +1 -1
  15. package/dist/assets/{InteractiveWhiteboard-6y5vg8O_.js → InteractiveWhiteboard-DZy7dsGY.js} +1 -1
  16. package/dist/assets/{LazyCourseware-wkNa0dIq.js → LazyCourseware-BpWRWiC9.js} +1 -1
  17. package/dist/assets/{LazyWhiteboard-Bjh_fdbw.js → LazyWhiteboard-GuLyGVWI.js} +1 -1
  18. package/dist/assets/{LessonEditorView-B_b0F9Cw.js → LessonEditorView-BPtkbV8_.js} +1 -1
  19. package/dist/assets/{LiveClassroomView-vm0rs4p7.js → LiveClassroomView-Cx5jD5H8.js} +1 -1
  20. package/dist/assets/{NotificationDetailModal-B3_95dku.js → NotificationDetailModal-DTwOttoS.js} +1 -1
  21. package/dist/assets/{PluginView-CZhQ0Ciy.js → PluginView-BB7sIiJf.js} +1 -1
  22. package/dist/assets/{QuizGeneratorModal-__riUCfV.js → QuizGeneratorModal-YoT3qVTl.js} +1 -1
  23. package/dist/assets/{StudentAssignmentView-BAL4_PJC.js → StudentAssignmentView-QIQtCByD.js} +1 -1
  24. package/dist/assets/{StudentLessonView-fXKI0uN9.js → StudentLessonView-ChP3ZGTo.js} +1 -1
  25. package/dist/assets/{StudentPreviewModal-DHgqiEHm.js → StudentPreviewModal-BIfgr0MU.js} +1 -1
  26. package/dist/assets/{StudentView-e7zUBtTA.js → StudentView-Clqr7kxh.js} +1 -1
  27. package/dist/assets/{SystemResourceLibraryModal-0MIIi5fd.js → SystemResourceLibraryModal-Cy012GB8.js} +1 -1
  28. package/dist/assets/{TeacherView-HxGS1psV.js → TeacherView-D-7hKKGD.js} +1 -1
  29. package/dist/assets/{index-CWqYoxFP.js → index-BqJrKVHF.js} +1 -1
  30. package/dist/assets/{index-CCKb8B5h.js → index-yp4O4BPB.js} +3 -3
  31. package/dist/index.html +1 -1
  32. package/dist/server.cjs +53 -5
  33. package/package.json +4 -1
package/cli.mjs CHANGED
@@ -2,36 +2,168 @@
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { dirname, join } from 'node:path';
6
- import { mkdirSync } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { mkdirSync, readFileSync, existsSync, rmSync } from 'node:fs';
7
7
  import os from 'node:os';
8
8
 
9
+ import { runClean } from './cli-cleaner.mjs';
10
+ import { runDoctor } from './cli-doctor.mjs';
11
+ import { runBackup, runRestore, runResetAdmin, runPluginsList } from './cli-data.mjs';
12
+
9
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
14
 
11
15
  const args = process.argv.slice(2);
12
- let port = null;
16
+ const firstArg = args[0] || '';
17
+
18
+ // ── Check Version ────────────────────────────────────────────────────────────
19
+ if (args.includes('-v') || args.includes('--version')) {
20
+ try {
21
+ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
22
+ console.log(`openlearn-next v${pkg.version}`);
23
+ } catch {
24
+ console.log('openlearn-next (version unknown)');
25
+ }
26
+ process.exit(0);
27
+ }
28
+
29
+ // ── Help Documentation ───────────────────────────────────────────────────────
30
+ if (args.includes('-h') || args.includes('--help')) {
31
+ console.log(`Usage: npx openlearn-next [command] [options]
32
+
33
+ Commands:
34
+ doctor 运行环境自检与健康诊断 (Node.js/硬件内存/存储权限/端口/网络)
35
+ backup [file] 一键数据库快照在线备份 (默认输出至当前目录 openlearn_backup_*.db)
36
+ restore <file> 从备份快照文件安全还原数据 (自动生成现有数据库回滚副本)
37
+ reset-admin [options] 重置系统管理员 (admin) 密码 (默认重置为 admin)
38
+ 选项: --password, -P <pwd>
39
+ plugins [list] 查看当前已安装的平台插件清单与加载状态
40
+ clean, clean-cache 清理 NPX 包缓存与本地临时运行数据
41
+ 选项:
42
+ --npx 仅清理 ~/.npm/_npx 中的 openlearn-next 历史包缓存
43
+ --db 重置本地 SQLite 数据库 (下次启动全新自动初始化)
44
+ --all 清理全部(NPX 历史包 + 运行临时日志 + 重置数据库)
45
+
46
+ Server Options:
47
+ -p, --port <port> 服务监听端口 (默认: 9000 或 process.env.PORT)
48
+ -H, --host <host> 绑定网络接口 IP (默认: 0.0.0.0 局域网全网监听)
49
+ -o, --open 服务就绪后自动唤起系统默认浏览器访问
50
+ --demo, --temp 临时演示沙盒模式:在系统临时目录创建一次性数据库,退出即自动销毁
51
+ --cors <origins> 允许跨域访问来源白名单 (多个以逗号分隔,如: "http://example.com,*")
52
+ --db-path <path> 自定义 SQLite 数据库文件绝对路径
53
+ -v, --version 显示版本号
54
+ -h, --help 显示此帮助信息
55
+
56
+ Environment Variables:
57
+ PORT HTTP 服务端口 (默认: 9000)
58
+ HOST 监听主机地址 (默认: 0.0.0.0)
59
+ OPENLEARN_DB_PATH SQLite 数据库绝对路径 (默认: ~/openlearn-next/data.db)
60
+ ALLOWED_ORIGINS CORS 白名单来源 (默认放行同源与开发环境)
61
+ GEMINI_API_KEY AI 助手兜底密钥 (推荐在管理员后台界面配置)
62
+ `);
63
+ process.exit(0);
64
+ }
65
+
66
+ // ── Extract Options Helper ──────────────────────────────────────────────────
67
+ let customPort = null;
68
+ let customHost = null;
69
+ let customDbPath = null;
70
+ let customCors = null;
71
+ const isDemo = args.includes('--demo') || args.includes('--temp');
72
+ const isOpen = args.includes('-o') || args.includes('--open');
13
73
 
14
74
  for (let i = 0; i < args.length; i++) {
15
75
  if (args[i] === '-p' || args[i] === '--port') {
16
- port = args[++i];
17
- } else if (args[i] === '-h' || args[i] === '--help') {
18
- console.log(`Usage: npx openlearn-next [options]
76
+ customPort = args[++i];
77
+ } else if (args[i] === '-H' || args[i] === '--host') {
78
+ customHost = args[++i];
79
+ } else if (args[i] === '--db-path') {
80
+ customDbPath = args[++i];
81
+ } else if (args[i] === '--cors') {
82
+ customCors = args[++i];
83
+ }
84
+ }
19
85
 
20
- Options:
21
- -p, --port <port> Port to listen on (default: 9000)
22
- -h, --help Show this help
86
+ if (customPort) process.env.PORT = customPort;
87
+ if (customHost) process.env.HOST = customHost;
88
+ if (customCors) process.env.ALLOWED_ORIGINS = customCors;
89
+ if (customDbPath) process.env.OPENLEARN_DB_PATH = resolve(customDbPath);
90
+ if (isOpen) process.env.OPEN_BROWSER = 'true';
23
91
 
24
- Environment:
25
- OPENLEARN_DB_PATH SQLite database path (default: ~/openlearn-next/data.db)
26
- GEMINI_API_KEY Optional. Fallback AI key; AI features are configured via AI Providers in the admin dashboard
27
- `);
28
- process.exit(0);
92
+ // ── Command: clean / clean-cache ─────────────────────────────────────────────
93
+ const isCleanCommand =
94
+ firstArg === 'clean' ||
95
+ firstArg === 'clean-cache' ||
96
+ args.includes('--clean') ||
97
+ args.includes('--clean-cache');
98
+
99
+ if (isCleanCommand) {
100
+ const cleanOptions = {
101
+ all: args.includes('--all'),
102
+ npx: args.includes('--npx'),
103
+ db: args.includes('--db'),
104
+ };
105
+ runClean(cleanOptions);
106
+ process.exit(0);
107
+ }
108
+
109
+ // ── Command: doctor ──────────────────────────────────────────────────────────
110
+ if (firstArg === 'doctor' || args.includes('--doctor')) {
111
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 9000;
112
+ const res = await runDoctor({ port, dbPath: process.env.OPENLEARN_DB_PATH });
113
+ process.exit(res.ok ? 0 : 1);
114
+ }
115
+
116
+ // ── Command: backup ──────────────────────────────────────────────────────────
117
+ if (firstArg === 'backup' || args.includes('--backup')) {
118
+ const backupIdx = args.findIndex((a) => a === 'backup' || a === '--backup');
119
+ let targetFile = undefined;
120
+ if (backupIdx !== -1 && args[backupIdx + 1] && !args[backupIdx + 1].startsWith('-')) {
121
+ targetFile = args[backupIdx + 1];
122
+ }
123
+ const res = await runBackup(targetFile, { dbPath: process.env.OPENLEARN_DB_PATH });
124
+ process.exit(res.ok ? 0 : 1);
125
+ }
126
+
127
+ // ── Command: restore ─────────────────────────────────────────────────────────
128
+ if (firstArg === 'restore' || args.includes('--restore')) {
129
+ const restoreIdx = args.findIndex((a) => a === 'restore' || a === '--restore');
130
+ let sourceFile = undefined;
131
+ if (restoreIdx !== -1 && args[restoreIdx + 1] && !args[restoreIdx + 1].startsWith('-')) {
132
+ sourceFile = args[restoreIdx + 1];
29
133
  }
134
+ const res = await runRestore(sourceFile, { dbPath: process.env.OPENLEARN_DB_PATH });
135
+ process.exit(res.ok ? 0 : 1);
30
136
  }
31
137
 
32
- if (port) process.env.PORT = port;
138
+ // ── Command: reset-admin ─────────────────────────────────────────────────────
139
+ if (firstArg === 'reset-admin' || args.includes('--reset-admin')) {
140
+ let newPassword = 'admin';
141
+ const pwdIdx = args.findIndex((a) => a === '--password' || a === '-P');
142
+ if (pwdIdx !== -1 && args[pwdIdx + 1] && !args[pwdIdx + 1].startsWith('-')) {
143
+ newPassword = args[pwdIdx + 1];
144
+ } else if (args[1] && !args[1].startsWith('-')) {
145
+ newPassword = args[1];
146
+ }
147
+ const res = await runResetAdmin(newPassword, { dbPath: process.env.OPENLEARN_DB_PATH });
148
+ process.exit(res.ok ? 0 : 1);
149
+ }
150
+
151
+ // ── Command: plugins / plugins list ──────────────────────────────────────────
152
+ if (firstArg === 'plugins' || args.includes('--plugins')) {
153
+ const res = await runPluginsList({ dbPath: process.env.OPENLEARN_DB_PATH });
154
+ process.exit(res.ok ? 0 : 1);
155
+ }
33
156
 
34
- if (!process.env.OPENLEARN_DB_PATH) {
157
+ // ── Normal Server Startup ────────────────────────────────────────────────────
158
+ let demoDir = null;
159
+
160
+ if (isDemo) {
161
+ demoDir = join(os.tmpdir(), `openlearn-demo-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`);
162
+ mkdirSync(demoDir, { recursive: true });
163
+ process.env.OPENLEARN_DB_PATH = join(demoDir, 'data.db');
164
+ console.log(`[openlearn-next] 临时沙盒模式已激活: ${demoDir}`);
165
+ console.log(`[openlearn-next] 提示: 退出程序时将自动销毁沙盒数据,不保留任何历史记录。`);
166
+ } else if (!process.env.OPENLEARN_DB_PATH) {
35
167
  const dataDir = join(os.homedir(), 'openlearn-next');
36
168
  mkdirSync(dataDir, { recursive: true });
37
169
  process.env.OPENLEARN_DB_PATH = join(dataDir, 'data.db');
@@ -40,7 +172,11 @@ if (!process.env.OPENLEARN_DB_PATH) {
40
172
  process.env.NODE_ENV = process.env.NODE_ENV || 'production';
41
173
 
42
174
  console.log(`[openlearn-next] PORT=${process.env.PORT || '9000'}`);
175
+ console.log(`[openlearn-next] HOST=${process.env.HOST || '0.0.0.0'}`);
43
176
  console.log(`[openlearn-next] DB=${process.env.OPENLEARN_DB_PATH}`);
177
+ if (process.env.ALLOWED_ORIGINS) {
178
+ console.log(`[openlearn-next] CORS_ALLOWED=${process.env.ALLOWED_ORIGINS}`);
179
+ }
44
180
 
45
181
  const serverPath = join(__dirname, 'dist', 'server.cjs');
46
182
  const child = spawn('node', [serverPath], {
@@ -48,6 +184,40 @@ const child = spawn('node', [serverPath], {
48
184
  env: { ...process.env },
49
185
  });
50
186
 
187
+ let cleanedUp = false;
188
+ const cleanup = () => {
189
+ if (cleanedUp) return;
190
+ cleanedUp = true;
191
+ if (isDemo && demoDir && existsSync(demoDir)) {
192
+ try {
193
+ rmSync(demoDir, { recursive: true, force: true });
194
+ console.log('\n[openlearn-next] 临时沙盒环境清理完毕。');
195
+ } catch {}
196
+ }
197
+ };
198
+
199
+ let isShuttingDown = false;
200
+ const handleSignal = (sig) => {
201
+ if (isShuttingDown) return;
202
+ isShuttingDown = true;
203
+ if (child && !child.killed) {
204
+ child.kill(sig);
205
+ // 3秒后强制保底退出
206
+ setTimeout(() => {
207
+ cleanup();
208
+ process.exit(0);
209
+ }, 3000).unref();
210
+ } else {
211
+ cleanup();
212
+ process.exit(0);
213
+ }
214
+ };
215
+
216
+ process.on('SIGINT', () => handleSignal('SIGINT'));
217
+ process.on('SIGTERM', () => handleSignal('SIGTERM'));
218
+ process.on('exit', cleanup);
219
+
51
220
  child.on('exit', (code) => {
221
+ cleanup();
52
222
  process.exit(code || 0);
53
223
  });
@@ -1 +1 @@
1
- import{j as e}from"./vendor-react-0Lg0vA-4.js";import{m as p}from"./index-CCKb8B5h.js";import"./vendor-icons-Dq6MhgH9.js";import"./vendor-pdf-Cp-cYteE.js";import"./vendor-utils-BEhuP0Zq.js";import"./vendor-motion-DjY2czvL.js";import"./vendor-pptx-B_NheIKl.js";import"./vendor-charts-CRXa4AlU.js";function w({batchPicker:o,setBatchPicker:t,batchPickerLesson:l,setBatchPickerLesson:a,batchPickerDate:i,setBatchPickerDate:n,batchPickerTargetClass:d,setBatchPickerTargetClass:c,lessons:x,classes:u,expandedClassId:m,confirmBatchPicker:h,lang:r}){return o?e.jsx("div",{className:"fixed inset-0 bg-gray-900/40 backdrop-blur-sm flex items-center justify-center p-6 z-50",children:e.jsxs(p.div,{initial:{opacity:0,scale:.95},animate:{opacity:1,scale:1},className:"bg-white border text-gray-900 border-gray-200 rounded-2xl shadow-2xl w-full max-w-md flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"p-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/80",children:[e.jsx("h2",{className:"font-bold text-gray-800 text-base",children:o==="schedule"?r==="zh"?"批量排课":"Batch Schedule":o==="lockedLesson"?r==="zh"?"批量设置锁定课程":"Batch Lock Lesson":r==="zh"?"批量转班":"Batch Transfer"}),e.jsx("button",{onClick:()=>t(null),className:"text-gray-400 hover:text-gray-600 text-lg font-bold p-1 hover:bg-gray-200 rounded",children:"×"})]}),e.jsxs("div",{className:"p-5 space-y-4",children:[(o==="schedule"||o==="lockedLesson")&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{className:"text-xs font-semibold text-gray-600",children:r==="zh"?"选择课程":"Select Lesson"}),e.jsxs("select",{value:l,onChange:s=>a(s.target.value),className:"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500",children:[e.jsx("option",{value:"",children:r==="zh"?"— 请选择 —":"— Select —"}),x.map(s=>e.jsx("option",{value:s.id,children:s.title},s.id))]})]}),o==="schedule"&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{className:"text-xs font-semibold text-gray-600",children:r==="zh"?"上课日期":"Schedule Date"}),e.jsx("input",{type:"date",value:i,onChange:s=>n(s.target.value),className:"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"})]}),o==="transfer"&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{className:"text-xs font-semibold text-gray-600",children:r==="zh"?"选择目标班级":"Select Target Class"}),e.jsxs("select",{value:d,onChange:s=>c(s.target.value),className:"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500",children:[e.jsx("option",{value:"",children:r==="zh"?"— 请选择 —":"— Select —"}),u.filter(s=>s.id!==m).map(s=>e.jsx("option",{value:s.id,children:s.name},s.id))]})]})]}),e.jsxs("div",{className:"p-4 border-t border-gray-100 flex justify-end gap-2",children:[e.jsx("button",{onClick:()=>t(null),className:"px-4 py-2 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-100 cursor-pointer",children:r==="zh"?"取消":"Cancel"}),e.jsx("button",{onClick:h,className:"px-4 py-2 text-sm rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 cursor-pointer",children:r==="zh"?"确认":"Confirm"})]})]})}):null}export{w as BatchPickerModal};
1
+ import{j as e}from"./vendor-react-0Lg0vA-4.js";import{m as p}from"./index-yp4O4BPB.js";import"./vendor-icons-Dq6MhgH9.js";import"./vendor-pdf-Cp-cYteE.js";import"./vendor-utils-BEhuP0Zq.js";import"./vendor-motion-DjY2czvL.js";import"./vendor-pptx-B_NheIKl.js";import"./vendor-charts-CRXa4AlU.js";function w({batchPicker:o,setBatchPicker:t,batchPickerLesson:l,setBatchPickerLesson:a,batchPickerDate:i,setBatchPickerDate:n,batchPickerTargetClass:d,setBatchPickerTargetClass:c,lessons:x,classes:u,expandedClassId:m,confirmBatchPicker:h,lang:r}){return o?e.jsx("div",{className:"fixed inset-0 bg-gray-900/40 backdrop-blur-sm flex items-center justify-center p-6 z-50",children:e.jsxs(p.div,{initial:{opacity:0,scale:.95},animate:{opacity:1,scale:1},className:"bg-white border text-gray-900 border-gray-200 rounded-2xl shadow-2xl w-full max-w-md flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"p-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/80",children:[e.jsx("h2",{className:"font-bold text-gray-800 text-base",children:o==="schedule"?r==="zh"?"批量排课":"Batch Schedule":o==="lockedLesson"?r==="zh"?"批量设置锁定课程":"Batch Lock Lesson":r==="zh"?"批量转班":"Batch Transfer"}),e.jsx("button",{onClick:()=>t(null),className:"text-gray-400 hover:text-gray-600 text-lg font-bold p-1 hover:bg-gray-200 rounded",children:"×"})]}),e.jsxs("div",{className:"p-5 space-y-4",children:[(o==="schedule"||o==="lockedLesson")&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{className:"text-xs font-semibold text-gray-600",children:r==="zh"?"选择课程":"Select Lesson"}),e.jsxs("select",{value:l,onChange:s=>a(s.target.value),className:"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500",children:[e.jsx("option",{value:"",children:r==="zh"?"— 请选择 —":"— Select —"}),x.map(s=>e.jsx("option",{value:s.id,children:s.title},s.id))]})]}),o==="schedule"&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{className:"text-xs font-semibold text-gray-600",children:r==="zh"?"上课日期":"Schedule Date"}),e.jsx("input",{type:"date",value:i,onChange:s=>n(s.target.value),className:"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"})]}),o==="transfer"&&e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{className:"text-xs font-semibold text-gray-600",children:r==="zh"?"选择目标班级":"Select Target Class"}),e.jsxs("select",{value:d,onChange:s=>c(s.target.value),className:"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500",children:[e.jsx("option",{value:"",children:r==="zh"?"— 请选择 —":"— Select —"}),u.filter(s=>s.id!==m).map(s=>e.jsx("option",{value:s.id,children:s.name},s.id))]})]})]}),e.jsxs("div",{className:"p-4 border-t border-gray-100 flex justify-end gap-2",children:[e.jsx("button",{onClick:()=>t(null),className:"px-4 py-2 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-100 cursor-pointer",children:r==="zh"?"取消":"Cancel"}),e.jsx("button",{onClick:h,className:"px-4 py-2 text-sm rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 cursor-pointer",children:r==="zh"?"确认":"Confirm"})]})]})}):null}export{w as BatchPickerModal};