- SQLite(bun:sqlite,编号迁移):班级、用户、会话(只存令牌哈希)、作品 - 登录取代统一访问口令:老师账号由配置设定;接口和 WebSocket 都要登录;按 IP 限制输错次数 - 班级管理页:建班、改名、删除(有学生时不能删);上传 CSV 名单导入(兼容 GBK / UTF-8, 识别表头,标出被 Excel 变成科学计数法的学号),密码留空生成 6 位数字;查看、重置密码, 导出带密码的名单 - 学生作品自动保存到服务器(按账号和实验),显示保存状态;离开页面前立即保存 - 前端加路由:登录页、实验页、班级管理页 - 部署:TEACHER_PASSWORD 必填,数据库放在数据卷 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
import type { ClientMessage, ServerMessage } from '@iot/shared'
|
|
import { createApp } from './app'
|
|
import { ensureTeacher, sessionToken, sessionUser } from './auth'
|
|
import { compileService } from './compile-service'
|
|
import { config } from './config'
|
|
import { openDatabase } from './db'
|
|
import { cleanupStaleRuns, RunSession } from './run-session'
|
|
import { checkSandbox, sandboxError } from './sandbox'
|
|
|
|
await cleanupStaleRuns()
|
|
await checkSandbox()
|
|
if (sandboxError) console.error(sandboxError)
|
|
|
|
const db = openDatabase()
|
|
ensureTeacher(db)
|
|
const app = createApp(db)
|
|
|
|
interface SocketData {
|
|
session?: RunSession
|
|
}
|
|
|
|
const server = Bun.serve({
|
|
port: config.port,
|
|
fetch(req, server) {
|
|
if (new URL(req.url).pathname === '/ws') {
|
|
if (!sessionUser(db, sessionToken(req.headers.get('cookie')))) return new Response('请先登录', { status: 401 })
|
|
if (server.upgrade(req, { data: {} })) return
|
|
return new Response('WebSocket 升级失败', { status: 400 })
|
|
}
|
|
return app.fetch(req, server)
|
|
},
|
|
websocket: {
|
|
data: {} as SocketData,
|
|
open(ws) {
|
|
ws.data.session = new RunSession((message) => ws.send(JSON.stringify(message)))
|
|
},
|
|
async message(ws, raw) {
|
|
let message: ClientMessage
|
|
try {
|
|
message = JSON.parse(String(raw))
|
|
} catch {
|
|
return
|
|
}
|
|
const session = ws.data.session
|
|
if (!session) return
|
|
if (message.type === 'run') {
|
|
try {
|
|
await session.start(message.files)
|
|
} catch (error) {
|
|
ws.send(JSON.stringify({ type: 'serverError', message: String(error) } satisfies ServerMessage))
|
|
}
|
|
} else if (message.type === 'stop') {
|
|
await session.stop()
|
|
} else if (message.type === 'boardInput') {
|
|
session.boardInput(message)
|
|
} else if (message.type === 'boardReset') {
|
|
session.boardReset()
|
|
} else {
|
|
session.input(message)
|
|
}
|
|
},
|
|
async close(ws) {
|
|
await ws.data.session?.dispose()
|
|
},
|
|
},
|
|
})
|
|
|
|
console.log(`服务器已启动:http://localhost:${server.port}`)
|
|
await compileService.start()
|
|
console.log('C# 编译服务已就绪')
|