openlearn-next 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # OpenLearnV2 — Educational OS
2
+
3
+ 插件驱动的在线教学平台(LMS),基于命令-事件总线架构设计,支持 AI Agent 辅助教学。
4
+
5
+ ## 技术栈
6
+
7
+ **前端:** React 19 · Vite 6 · TailwindCSS 4 · TypeScript 5.8 · Zustand · Konva
8
+ **后端:** Express 4 · better-sqlite3 · Socket.IO · Node.js Worker Threads
9
+ **AI:** Gemini / OpenAI 兼容 API
10
+ **部署:** PM2 + Nginx + Docker
11
+
12
+ ## 快速开始
13
+
14
+ ### 通过 npx 一键启动
15
+
16
+ ```bash
17
+ # 首次会自动下载安装,无需 clone 项目
18
+ export GEMINI_API_KEY=你的密钥
19
+ npx openlearn-next
20
+
21
+ # 可选参数
22
+ npx openlearn-next -p 3000 # 自定义端口(默认 9000)
23
+ OPENLEARN_DB_PATH=./my.db npx openlearn-next # 自定义数据库路径(默认 ~/openlearn-next/data.db)
24
+ ```
25
+
26
+ 默认账号:`admin` / `admin`(管理员),`teacher` / `teacher`(教师)。
27
+
28
+ ### 本地开发
29
+
30
+ ```bash
31
+ # 安装依赖
32
+ npm install
33
+
34
+ # 配置环境变量
35
+ echo "GEMINI_API_KEY=你的密钥" > .env
36
+
37
+ # 启动开发服务
38
+ ./dev.sh
39
+ # 或:npm run dev
40
+
41
+ # 访问
42
+ open http://localhost:9000
43
+ ```
44
+
45
+ 默认账号:`admin` / `admin`(管理员),`teacher` / `teacher`(教师)
46
+
47
+ ## 生产部署
48
+
49
+ ```bash
50
+ chmod +x deploy.sh
51
+ ./deploy.sh
52
+ ```
53
+
54
+ 脚本自动完成:构建 → 生成 Nginx 配置 → 配置 PM2 → 生成加密密钥。
55
+ 访问 `http://服务器IP` 或通过 Nginx 反向代理。
56
+
57
+ ## 项目结构
58
+
59
+ ```
60
+ ├── server.ts # Express + Socket.IO + API 路由
61
+ ├── packages/
62
+ │ ├── core/ # OS 内核
63
+ │ │ ├── kernel/ # 内核容器,组装子系统
64
+ │ │ ├── command-bus/ # 命令总线(注册 handler,执行命令)
65
+ │ │ ├── event-bus/ # 事件总线(发布/订阅,审计日志)
66
+ │ │ ├── registry/ # AI Agent 工具注册表
67
+ │ │ ├── capability-system/ # 权限守卫(RBAC)
68
+ │ │ ├── plugin-host/ # 插件生命周期 + 上下文构建
69
+ │ │ ├── worker-runtime/ # Worker Thread 隔离执行
70
+ │ │ ├── esm-loader/ # ESM 动态加载(data: URL)
71
+ │ │ ├── di/ # Token 依赖注入容器
72
+ │ │ ├── db/ # SQLite 数据库(30+ 表)
73
+ │ │ └── process-manager/ # 后台进程/定时任务
74
+ │ └── plugins/ # 内置插件
75
+ │ ├── builtin.ts # 课程·白板·课件·插件管理
76
+ │ ├── management.ts # 班级·学生·作业·排课·考勤
77
+ │ ├── vfs.ts # 虚拟文件系统
78
+ │ ├── process.ts # 进程管理
79
+ │ ├── ai-planner.ts # AI 自动规划
80
+ │ ├── ai-submit-injector.ts # LMS SDK 注入课件
81
+ │ └── assignment-eval.ts # 作业提交·互评·评分
82
+ ├── src/
83
+ │ ├── App.tsx # 主应用组件
84
+ │ ├── components/ # UI 组件(24 个)
85
+ │ ├── features/ # 业务模块(白板·课件)
86
+ │ ├── services/ # 前端服务(EventBus·Socket·API)
87
+ │ ├── store/ # Zustand 状态管理
88
+ │ └── hooks/ # 自定义 Hooks
89
+ ├── server/ # 服务端模块(自 v5.0)
90
+ │ ├── middleware/auth.ts # 认证中间件
91
+ │ ├── routes/auth.ts # 认证路由
92
+ │ └── utils/ # 工具(加密·迁移·日志·上传)
93
+ ├── nginx.conf # Nginx 配置模板
94
+ ├── deploy.sh # 一键部署脚本
95
+ ├── ecosystem.config.cjs # PM2 配置
96
+ ├── Dockerfile
97
+ └── docker-compose.yml
98
+ ```
99
+
100
+ ## 核心架构
101
+
102
+ ### OS 内核
103
+
104
+ `Kernel` 是全局单例,组装 10 个子系统:
105
+
106
+ | 层 | 子系统 | 职责 |
107
+ |----|--------|------|
108
+ | L0 | EventBus | 发布/订阅,通配符匹配,自动审计日志 |
109
+ | L0 | CapabilityGuard | 字符串 RBAC(`lesson:write`、`*:*:*`) |
110
+ | L0 | ServiceRegistry | Token DI 容器(拓扑排序,循环检测) |
111
+ | L1 | CommandBus | 命令执行管线 + 拦截器链(权限 + 高危审批) |
112
+ | L1 | ActionRegistry | AI Agent 工具注册表 |
113
+ | L2 | ProcessManager | 后台进程/定时任务 |
114
+ | L2 | EsmLoader | ESM 动态加载(Node.js data: URL) |
115
+ | L2 | PluginHost | 插件生命周期管理 + 热重载 |
116
+ | L3 | WorkerManager | Worker Thread 隔离 + RPC 代理 |
117
+ | — | DB | SQLite WAL 模式,30+ 张表 |
118
+
119
+ ### 插件系统
120
+
121
+ 插件以 ZIP 包分发,通过 PluginCenter 上传安装:
122
+
123
+ ```
124
+ ext-exam.zip
125
+ ├── manifest.json # id, name, version, capabilitiesProposed, classroomTools
126
+ ├── server/index.js # activate(ctx) → 注册 CommandHandler + Action
127
+ └── frontend/ # React 组件(挂载到 Extension Slot)
128
+ ```
129
+
130
+ **插件能力(v5.1):**
131
+ - 命令注册 + AI Agent 工具注册
132
+ - 事件发布/订阅 + 审计日志
133
+ - 自建数据库表(`ctx.db.ensureTable()`)
134
+ - 共享依赖引用(`ctx.require('recharts')`)
135
+ - 前端 Extension Slot(`teacher.panel` 等 8 个)
136
+ - Worker Thread 隔离执行
137
+
138
+ **已安装的课堂工具:** 选择题测验 · 专业测验 · 随机点名 · 思维导图 · 计时器 · 代码沙箱 · 数学图形
139
+
140
+ ### 事件通信
141
+
142
+ ```
143
+ 教师操作白板
144
+ ├─ 实时绘制(高频)→ Socket.IO 直连(< 500ms)
145
+ └─ 结构操作 → frontendEventBus → SocketBridge → server EventBus
146
+ ├─ SQLite events 表(审计)
147
+ └─ io.to(room).emit()(其他客户端)
148
+ ```
149
+
150
+ ## npm 脚本
151
+
152
+ | 命令 | 说明 |
153
+ |------|------|
154
+ | `npx openlearn-next` | 一键启动生产服务 |
155
+ | `npm run dev` | 启动开发服务 |
156
+ | `npm run build` | 生产构建(Vite + esbuild) |
157
+ | `npm start` | 运行生产构建 |
158
+ | `npm test` | 运行测试 |
159
+ | `npm run lint` | TypeScript 类型检查 |
160
+
161
+ ## 环境变量
162
+
163
+ | 变量 | 必需 | 说明 |
164
+ |------|:--:|------|
165
+ | `GEMINI_API_KEY` | ✅ | 默认 AI 服务密钥(也可在管理面板配置第三方 AI) |
166
+ | `PORT` | — | 服务端口,默认 9000 |
167
+ | `OPENLEARN_DB_PATH` | — | SQLite 数据库路径(npx 默认 ~/openlearn-next/data.db,本地开发默认项目目录) |
168
+ | `ENCRYPTION_KEY` | ✅ | 64 位 hex,AI Provider API Key 加密密钥(`deploy.sh` 自动生成) |
169
+ | `LOG_LEVEL` | — | 日志级别(debug / info / warn / error),默认 info |
170
+ | `ALLOWED_ORIGINS` | — | CORS 白名单,逗号分隔 |
package/cli.mjs ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ import { mkdirSync } from 'node:fs';
7
+ import os from 'node:os';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+
11
+ // ── CLI args ───────────────────────────────────────────────────────
12
+ const args = process.argv.slice(2);
13
+ let port = null;
14
+
15
+ for (let i = 0; i < args.length; i++) {
16
+ if (args[i] === '-p' || args[i] === '--port') {
17
+ port = args[++i];
18
+ } else if (args[i] === '-h' || args[i] === '--help') {
19
+ console.log(`Usage: npx openlearn-next [options]
20
+
21
+ Options:
22
+ -p, --port <port> Port to listen on (default: 9000)
23
+ -h, --help Show this help
24
+
25
+ Environment:
26
+ OPENLEARN_DB_PATH SQLite database path (default: ~/openlearn-next/data.db)
27
+ GEMINI_API_KEY Google Gemini API key for AI features
28
+ `);
29
+ process.exit(0);
30
+ }
31
+ }
32
+
33
+ // ── Env setup ──────────────────────────────────────────────────────
34
+ if (port) process.env.PORT = port;
35
+
36
+ if (!process.env.OPENLEARN_DB_PATH) {
37
+ const dataDir = join(os.homedir(), 'openlearn-next');
38
+ mkdirSync(dataDir, { recursive: true });
39
+ process.env.OPENLEARN_DB_PATH = join(dataDir, 'data.db');
40
+ }
41
+
42
+ console.log(`[openlearn-next] PORT=${process.env.PORT || '9000'}`);
43
+ console.log(`[openlearn-next] DB=${process.env.OPENLEARN_DB_PATH}`);
44
+
45
+ // ── Start server ───────────────────────────────────────────────────
46
+ const serverPath = join(__dirname, 'dist', 'server.cjs');
47
+ const child = spawn('node', [serverPath], {
48
+ stdio: 'inherit',
49
+ env: { ...process.env },
50
+ });
51
+
52
+ child.on('exit', (code) => {
53
+ process.exit(code || 0);
54
+ });
@@ -0,0 +1 @@
1
+ import{r as m}from"./vendor-konva-BRP-lFzZ.js";import{j as e,G as h,M as v,a as w,X as y}from"./whiteboard-FFuMxeyx.js";const j=(function(){const l=typeof document<"u"&&document.createElement("link").relList;return l&&l.supports&&l.supports("modulepreload")?"modulepreload":"preload"})(),E=function(n){return"/"+n},p={},N=function(l,c,f){let i=Promise.resolve();if(c&&c.length>0){let t=function(r){return Promise.all(r.map(a=>Promise.resolve(a).then(u=>({status:"fulfilled",value:u}),u=>({status:"rejected",reason:u}))))};document.getElementsByTagName("link");const s=document.querySelector("meta[property=csp-nonce]"),x=s?.nonce||s?.getAttribute("nonce");i=t(c.map(r=>{if(r=E(r),r in p)return;p[r]=!0;const a=r.endsWith(".css"),u=a?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${r}"]${u}`))return;const o=document.createElement("link");if(o.rel=a?"stylesheet":j,a||(o.as="script"),o.crossOrigin="",o.href=r,x&&o.setAttribute("nonce",x),document.head.appendChild(o),a)return new Promise((g,b)=>{o.addEventListener("load",g),o.addEventListener("error",()=>b(new Error(`Unable to preload CSS for ${r}`)))})}))}function d(t){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=t,window.dispatchEvent(s),!s.defaultPrevented)throw t}return i.then(t=>{for(const s of t||[])s.status==="rejected"&&d(s.reason);return l().catch(d)})},_=m.lazy(()=>N(()=>Promise.resolve().then(()=>F),void 0).then(n=>({default:n.InteractiveCoursewareViewer}))),P=Object.freeze(Object.defineProperty({__proto__:null,CoursewareViewer:_},Symbol.toStringTag,{value:"Module"}));function S({coursewareId:n,onClose:l}){const[c,f]=m.useState(!1),i=m.useRef(null);m.useEffect(()=>{const t=()=>{f(!!document.fullscreenElement&&document.fullscreenElement===i.current)};return document.addEventListener("fullscreenchange",t),()=>{document.removeEventListener("fullscreenchange",t)}},[]);const d=()=>{const t=i.current;t&&(document.fullscreenElement?document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen&&document.webkitExitFullscreen():t.requestFullscreen?t.requestFullscreen():t.webkitRequestFullscreen&&t.webkitRequestFullscreen())};return n?e.jsxs("div",{ref:i,className:"flex flex-col h-full bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden relative",children:[e.jsxs("div",{className:"flex items-center justify-between px-4 py-2 bg-gray-100 border-b border-gray-200 shrink-0",children:[e.jsxs("div",{className:"flex items-center gap-2 select-none",children:[e.jsx(h,{size:16,className:"text-indigo-500"}),e.jsx("span",{className:"font-semibold text-sm text-gray-700",children:"Interactive Courseware"})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{onClick:d,className:"p-1 hover:bg-gray-200 rounded-lg text-gray-500 transition-colors cursor-pointer",title:c?"退出全屏":"全屏播放",children:c?e.jsx(v,{size:16}):e.jsx(w,{size:16})}),l&&e.jsx("button",{onClick:l,className:"p-1 hover:bg-gray-200 rounded-lg text-gray-500 transition-colors cursor-pointer",children:e.jsx(y,{size:16})})]})]}),e.jsx("div",{className:"flex-1 relative bg-white",children:e.jsx("iframe",{src:`/api/courseware/${n}`,sandbox:"allow-scripts allow-downloads allow-forms",allowFullScreen:!0,className:"w-full h-full border-none",title:"Interactive Courseware"})})]}):e.jsxs("div",{className:"flex flex-col items-center justify-center h-full text-gray-400 bg-gray-50 rounded-xl border border-dashed border-gray-300",children:[e.jsx(h,{size:48,className:"mb-4 text-gray-300 opacity-50"}),e.jsx("h3",{className:"text-lg font-medium text-gray-700",children:"No Courseware Selected"}),e.jsx("p",{className:"mt-2 text-sm text-center",children:"Please select a courseware from the list to view it interactively."})]})}const F=Object.freeze(Object.defineProperty({__proto__:null,InteractiveCoursewareViewer:S},Symbol.toStringTag,{value:"Module"}));export{N as _,P as i};