feat(courseware,docs): 完善课件传输与岛编辑并统一全仓文档
- 增加课件上传、班级可见范围、设备补收与传输进度 - 整合自定义灵动岛编辑器和相关客户端交互 - 同步三端契约、数据库迁移、部署配置与隔离回归 - 按源码统一文档与官网说明,移除过时任务书和审查记录 - 保持产品版本为 1.4.0
This commit is contained in:
1 parent
536379036a
commit
c7f215a09c
127 files changed
+4334
-8482
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in new issue
Block a user