feat(courseware,docs): 完善课件传输与岛编辑并统一全仓文档

- 增加课件上传、班级可见范围、设备补收与传输进度
- 整合自定义灵动岛编辑器和相关客户端交互
- 同步三端契约、数据库迁移、部署配置与隔离回归
- 按源码统一文档与官网说明,移除过时任务书和审查记录
- 保持产品版本为 1.4.0
This commit is contained in:
LoveChengke committed 2026-10-18 11:53:37 +08:00
1 parent 536379036a
commit c7f215a09c
127 files changed
+4334 -8482

No files matched your search

+6 -7
View File
@@ -170,12 +170,11 @@ export const DEFAULT_LETTER_LEVELS: readonly string[] = ['A', 'B', 'C', 'D'];
/**
* ClassHelper 班级端在线判定窗口(毫秒)。
*
* 插件默认 60 秒打一次心跳,因此「最近 60 秒内有心跳 = 在线」是最贴合的口径;
* 判定与展示(教师网页的「ClassHelper 在线状态」)共用这一个常量,避免两处各写一个数。
* @deprecated 旧联动在线窗口兼容导出,当前设备在线状态使用 CLIENT_ONLINE_WINDOW_MS。
*/
export const CLASS_HELPER_ONLINE_WINDOW_MS = 60_000;
/** 教师网页「ClassHelper 在线状态」的自动刷新间隔(秒) */
/** @deprecated 旧联动状态页刷新间隔,现行设备页不使用。 */
export const CLASS_HELPER_STATUS_REFRESH_SECONDS = 30;
/** 通知优先级 */
@@ -199,8 +198,7 @@ export const PRIORITY_TAG_TYPES: Record<NotificationPriority, 'info' | 'primary'
/**
* 优先级权重(越大越重要)。
*
* 灵动岛有多条通知时按它排序("默认按重要程度排列"),插件合并同 id 消息时也用它取更高优先级 ——
* 两边必须同一份口径,否则会出现"列表里的顺序和合并后的优先级不一致"。
* 灵动岛列表排序和同 id 消息合并共用,保证展示顺序与优先级合并一致。
*/
export const PRIORITY_RANK: Record<NotificationPriority, number> = {
LOW: 0,
@@ -480,6 +478,7 @@ export const DEFAULT_WEEK_COUNT = 20;
/** WebSocket 事件名(服务端广播 / 客户端监听保持一致) */
export const SOCKET_EVENTS = {
coursewareChanged: 'courseware:changed',
notificationNew: 'notification:new',
notificationDeleted: 'notification:deleted',
homeworkNew: 'homework:new',
@@ -489,7 +488,7 @@ export const SOCKET_EVENTS = {
scheduleUpdated: 'schedule:updated',
clientReleaseProgress: 'client:release-progress',
classUpdated: 'class:updated',
/** "叫人":老师点名让某位同学去找他(定向到 user:{studentId} 房间) */
/** "叫人":老师点名让某位同学去找他(定向到班级会话 session:{classId} 房间) */
callNew: 'call:new',
connected: 'connected',
} as const;
@@ -599,7 +598,7 @@ export const MAX_PAGE_SIZE = 200;
/* ------------------------------------------------------------------ 更新检查 */
/**
* 更新来源:本项目用 **GitHub Releases** 分发交付物(服务端 / 客户端安装包、便携版、插件 .cipx),
* 更新来源:本项目用 **GitHub Releases** 分发交付物(服务端 / 客户端安装包与便携版),
* 因此"有没有新版本"就是问 GitHub 上最新的那个 Release。
*
* 仓库是**公开**的,匿名调用即可(不需要 token);但匿名限流是 60 次/小时/IP,
+63
View File
@@ -0,0 +1,63 @@
/** 课件契约:文件先保存到服务端,再向可见班级下发下载任务。 */
export const COURSEWARE_MAX_BYTES = 512 * 1024 * 1024;
export const COURSEWARE_TRANSFER_TIMEOUT_MS = 30 * 60_000;
export const COURSEWARE_ISLAND_SIZE = { width: 424, height: 132 } as const;
export const COURSEWARE_CATEGORIES = {
presentation: '演示文稿',
document: '文档',
spreadsheet: '表格',
image: '图片',
video: '视频',
audio: '音频',
archive: '压缩包',
other: '其他',
} as const;
export type CoursewareCategory = keyof typeof COURSEWARE_CATEGORIES;
export function coursewareCategory(name: string): CoursewareCategory {
const ext = name.split('.').pop()?.toLowerCase() ?? '';
if (['ppt', 'pptx', 'odp', 'key'].includes(ext)) return 'presentation';
if (['pdf', 'doc', 'docx', 'txt', 'md', 'odt', 'rtf'].includes(ext)) return 'document';
if (['xls', 'xlsx', 'csv', 'ods'].includes(ext)) return 'spreadsheet';
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) return 'image';
if (['mp4', 'webm', 'avi', 'mov', 'mkv', 'wmv'].includes(ext)) return 'video';
if (['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext)) return 'audio';
if (['zip', '7z', 'rar', 'tar', 'gz'].includes(ext)) return 'archive';
return 'other';
}
export interface CoursewareClassDto {
id: string;
name: string;
}
export interface CoursewareDto {
id: string;
name: string;
size: number;
category: CoursewareCategory;
extension: string;
archiveDate: string;
sha256: string;
createdAt: string;
teacher: { id: string; name: string };
classes: CoursewareClassDto[];
}
export interface CoursewareUploadRequest {
name: string;
size: number;
classIds: string[];
mode: 'cloud' | 'push';
}
export interface CoursewarePushDto {
id: string;
courseware: CoursewareDto;
}
export interface CoursewareTransfer {
id: string;
name: string;
category: CoursewareCategory;
teacherName: string;
receivedBytes: number;
totalBytes: number;
progress: number;
stage: 'downloading' | 'verifying' | 'completed' | 'failed';
error?: string;
}
+1
View File
@@ -9,3 +9,4 @@ export * from './utils.js';
export * from './class-plan.js';
export * from './island-layout.js';
export type * from './types.js';
export * from './courseware.js';
+13 -3
View File
@@ -106,7 +106,7 @@ export function canManageSubjectContent(input: {
isSubjectTeacherOfCourse: boolean;
/**
* 是否允许 ClassHelper 班级端代写。
* 教室机器可以录**作业**(老师在教室随手录一条很常见,见 AGENTS.md §5 第 20 条),
* 教室机器可以录**作业**(老师在教室随手录一条很常见,见 AGENTS.md 的现行工程约定),
* 但**成绩**不给 —— 成绩是面向家长/学生的评价数据,仍由教师在 Web 端录入。
*/
allowClassDevice?: boolean;
@@ -160,9 +160,11 @@ export function canImport(role: UserRole, classRole: ClassRole, kind: ImportKind
* - 桌面客户端:功能入口显隐。
*
* 键名用 `领域:动作` 的形式,新增功能要**先在这里加键**,再去两端用。
* **不要**在页面或服务里硬编码角色判断来代替它(AGENTS.md §5 第 3 条)。
* **不要**在页面或服务里硬编码角色判断来代替它(AGENTS.md 的现行工程约定)。
*/
export const PERMISSION_KEYS = [
'courseware:read',
'courseware:write',
// 班级与人员
'class:read',
'class:manage',
@@ -189,7 +191,7 @@ export const PERMISSION_KEYS = [
'import:teachers',
'import:schedule',
'import:class-teachers',
// 设备与联动
// 客户端设备维护
'client:read',
'client:manage',
'client:update',
@@ -214,6 +216,8 @@ export type PermissionKey = (typeof PERMISSION_KEYS)[number];
/** 默认角色组之外的教师岗位组;空年级范围必须由管理员配置,不能自动授权全校。 */
const TEACHING_KEYS: PermissionKey[] = [
'courseware:read',
'courseware:write',
'homework:write',
'grade:write',
'notification:publish',
@@ -249,6 +253,8 @@ export const BUILTIN_TEACHER_GROUPS = [
/** 权限键的中文名(权限组管理页与文档展示,别再各写一份) */
export const PERMISSION_LABELS: Record<PermissionKey, string> = {
'courseware:read': '查看与下载课件',
'courseware:write': '上传、设置可见范围与推送自己的课件',
'class:read': '查看班级与班级列表',
'class:manage': '创建 / 修改 / 删除班级(含班级账号密码)',
'roster:read': '查看学生名单',
@@ -309,6 +315,8 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
{
title: '内容发布',
keys: [
'courseware:read',
'courseware:write',
'homework:read',
'homework:write',
'notification:read',
@@ -359,6 +367,8 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
* (`canManageSubjectContent(..., { allowClassDevice })` 等)决定,不受权限组影响。
*/
export const PERMISSION_DEFAULT_ROLES: Record<PermissionKey, readonly UserRole[]> = {
'courseware:read': ['ADMIN', 'TEACHER'],
'courseware:write': ['ADMIN', 'TEACHER'],
'class:read': ['ADMIN', 'TEACHER'],
'class:manage': ['ADMIN'],
'roster:read': ['ADMIN', 'TEACHER'],
+5 -2
View File
@@ -1,3 +1,4 @@
import type { CoursewareTransfer } from './courseware.js';
import type {
PermissionScope as ContractPermissionScope,
ReleaseProgressDto as ContractReleaseProgressDto,
@@ -1156,6 +1157,7 @@ export type IslandStyle = 'black' | 'glass' | 'tinted';
/** 灵动岛当前状态(主进程持有,渲染进程与冒烟测试读取) */
export interface IslandState {
coursewareTransfer?: CoursewareTransfer | null;
mode: IslandMode;
lessonReminder?: IslandLessonReminder | null;
/** 正在展示的通知(展开态或胶囊态) */
@@ -1172,7 +1174,7 @@ export interface IslandState {
* `closing` 是"用户点了知道了 / 标为已读"之后的**收回动画**阶段:状态在同一帧就已经落地
* (`active` 清空、`queued` 清空),主进程只是把形态切回胶囊让渲染进程把卡片收回去,
* 待 `CLOSE_ANIMATION_MS` 之后再淡出隐藏。它不是跨进程握手,渲染进程也不需要任何回执
* (见 AGENTS.md §5 第 34 条)。
* (见 AGENTS.md 的现行工程约定)。
*/
reason: 'new' | 'after-class' | 'urgent' | 'call' | 'closing' | null;
/**
@@ -1208,6 +1210,7 @@ export interface DashboardSummary {
/** 服务端 -> 客户端事件名 */
export interface ServerToClientEvents {
'courseware:changed': (payload: { id: string }) => void;
'client:release-progress': (payload: ContractReleaseProgressDto) => void;
'notification:deleted': (payload: { id: string; classId: string }) => void;
'notification:new': (payload: NotificationDto) => void;
@@ -1515,7 +1518,7 @@ export interface AssignUserGroupsRequest {
/**
* `GET /api/permissions/me`:当前账号的生效权限。
*
* 两端用它决定"入口显不显示",而**不是**再写一套权限规则(AGENTS.md §5 第 3 条):
* 两端用它决定"入口显不显示",而**不是**再写一套权限规则(AGENTS.md 的现行工程约定):
* 真正的拦截始终在服务端。
*/
export interface MyPermissionsDto {
+2 -2
View File
@@ -531,7 +531,7 @@ export function isNewerVersion(
* 客户端安装包的**文件名约定**。
*
* 与 GitHub Release 上上传的 ASCII 名、以及官网下载页的离线兜底直链
* **必须一致**(AGENTS.md §4.4:改名要三处一起改)。
* **必须一致**(AGENTS.md 的现行工程约定:改名要三处一起改)。
*/
export function clientReleaseFileName(version: string): string {
return `ClassHelper-${normalizeVersion(version)}-x64-client-setup.exe`;
@@ -541,7 +541,7 @@ export function clientReleaseFileName(version: string): string {
* 服务端从 GitHub 拉取客户端安装包的直链。
*
* 服务端替教室机器"代下"再缓存(教室常常没有外网)—— Web 端**不能**直连 GitHub
* (CSP 的 connect-src 只有 self / ws / CORS_ORIGIN,见 AGENTS.md §5 第 52 条)。
* (CSP 的 connect-src 只有 self / ws / CORS_ORIGIN,见 AGENTS.md 的现行工程约定)。
*/
export function clientReleaseDownloadUrl(version: string): string {
const tag = `v${normalizeVersion(version)}`;