chore(release): 1.3.3 发布并退役旧联动

This commit is contained in:
LoveChengke committed 2026-10-17 03:06:46 +08:00
1 parent b7cdc7c9ea
commit c0ebf88138
184 files changed
+5450 -11705

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@classhelper/shared",
"version": "1.3.2",
"version": "1.3.3",
"private": true,
"description": "ClassHelper - 三端共享的类型、常量与工具函数",
"type": "module",
+4 -53
View File
@@ -34,8 +34,6 @@ export const API_PATHS = {
archives: '/archives',
/** 学期周次:逐周日期区间、按开学日期自动生成、联网拉取调休建议(仅管理员) */
term: '/term',
/** ClassIsland 联动:设备接入、状态上报、通知下发 / ClassHelper 班级端在线状态 */
integrations: '/integrations',
/** 数据库管理(仅管理员):状态/备份/导入导出/一键切换 */
database: '/database',
/** 更新检查:Web 端经服务端转发查 GitHub 上的最新 Release(浏览器受 CSP 限制不能直连) */
@@ -89,12 +87,7 @@ export const CLASS_TEACHER_ROLE_LABELS: Record<ClassTeacherRole, string> = {
/* ------------------------------------------------------------------ 学生名单 */
export const STUDENT_STATUSES: readonly StudentStatus[] = [
'active',
'graduated',
'transferred',
'inactive',
];
export const STUDENT_STATUSES: readonly StudentStatus[] = ['active', 'graduated', 'transferred', 'inactive'];
export const STUDENT_STATUS_LABELS: Record<StudentStatus, string> = {
active: '在读',
@@ -488,6 +481,7 @@ export const DEFAULT_WEEK_COUNT = 20;
/** WebSocket 事件名(服务端广播 / 客户端监听保持一致) */
export const SOCKET_EVENTS = {
notificationNew: 'notification:new',
notificationDeleted: 'notification:deleted',
homeworkNew: 'homework:new',
homeworkUpdated: 'homework:updated',
homeworkStatus: 'homework:status',
@@ -496,17 +490,10 @@ export const SOCKET_EVENTS = {
classUpdated: 'class:updated',
/** "叫人":老师点名让某位同学去找他(定向到 user:{studentId} 房间) */
callNew: 'call:new',
/** ClassIsland 联动:设备状态上报(Web 端实时展示当前课程) */
classislandState: 'classisland:state',
/** ClassIsland 联动:教师发起的提醒已下发(插件据此全屏弹出) */
classislandNotification: 'classisland:notification',
connected: 'connected',
} as const;
/** ClassIsland 联动:设备接入令牌的前缀(便于识别与日志排查) */
export const CLASSISLAND_DEVICE_TOKEN_PREFIX = 'chci_';
/** ClassIsland 联动:ClassIsland 的时间点类型(与 TimeLayoutItem.TimeType 一致) */
/** ClassIsland JSON 导入:时间点类型(与 TimeLayoutItem.TimeType 一致) */
export const CLASSISLAND_TIME_TYPES = {
class: 0,
break: 1,
@@ -525,38 +512,6 @@ export const CLASSISLAND_TIME_TYPE_LABELS: Record<number, string> = {
3: '行动',
};
/** ClassIsland TimeState 字符串 -> 中文(插件上报 → Web 端展示) */
export const CLASSISLAND_TIME_STATE_LABELS: Record<string, string> = {
None: '空闲',
OnClass: '上课中',
PrepareOnClass: '预备铃',
Breaking: '课间休息',
AfterSchool: '已放学',
};
/**
* 通知的显示位置(由**教室的班级客户端**在设置页里选,存在班级上)。
*
* - `both`:ClassHelper 客户端弹(弹窗 + 灵动岛),同时推给教室的 ClassIsland;
* - `client`:只在 ClassHelper 客户端弹,不打扰 ClassIsland;
* - `classisland`:只在 ClassIsland 上弹(客户端只进通知中心,不弹窗、不上岛)。
*/
export const CLASSISLAND_NOTIFICATION_CHANNELS = ['both', 'client', 'classisland'] as const;
export const CLASSISLAND_NOTIFICATION_CHANNEL_LABELS: Record<string, string> = {
both: '两者都弹',
client: '灵动岛(ClassHelper 客户端)',
classisland: 'ClassIsland',
};
export const CLASSISLAND_NOTIFICATION_CHANNEL_HINTS: Record<string, string> = {
both: '灵动岛与 ClassIsland 都会提醒老师发的内容(默认)',
client: '只在 ClassHelper 客户端的灵动岛提醒,不推送到 ClassIsland',
classisland: '只推送到 ClassIsland 全屏提醒,灵动岛不弹(通知中心仍留记录)',
};
/** 默认显示位置:两端都弹 */
export const DEFAULT_CLASSISLAND_NOTIFICATION_CHANNEL = 'both';
/**
* 作业录入的默认快捷短语(客户端「设置 → 作业录入」可增删)。
*
@@ -582,10 +537,6 @@ export const HOMEWORK_PHRASE_MAX_LENGTH = 20;
/** 快捷短语最多几条 */
export const HOMEWORK_PHRASE_MAX_COUNT = 24;
/** 默认提醒显示时长(秒),后端与插件共用同一口径 */
export const CLASSISLAND_NOTIFICATION_DEFAULT_DURATION = 8;
export const CLASSISLAND_NOTIFICATION_MAX_DURATION = 120;
/**
* "叫人"快捷短语(Web 管理端一键选择,ClassHelper 班级端灵动岛同步展示)。
* 自定义消息会替换/补全这些短语;`message` 为空时用短语本身。
@@ -677,7 +628,7 @@ export const UPDATE_CHECK_TIMEOUT_MS = 8_000;
*
* 为什么**不用 Socket.IO 事件**下发更新与远控:教室机的连接经常断(代理、休眠、重启),
* 而「这台机器什么版本、要不要更新、有没有待执行命令」必须下次上线就能补上,
* 所以统一用「DB 记状态 + 客户端定时拉取」的范式(与 `IntegrationDevice.requestScheduleReport` 同一套路),
* 所以统一用「DB 记状态 + 客户端定时拉取」的范式,
* 不引入新的长连接握手。
*/
export const CLIENT_HEARTBEAT_INTERVAL_MS = 30_000;
+27 -21
View File
@@ -71,7 +71,9 @@ export function canManageCourses(classRole: ClassRole): boolean {
* 因此凡是与本班有关联的教师(班主任或任课老师)都能发;ClassHelper 班级端也可以。
*/
export function canPublishNotification(classRole: ClassRole): boolean {
return classRole === 'ADMIN' || classRole === 'HEAD' || classRole === 'SUBJECT' || classRole === 'CLASS_DEVICE';
return (
classRole === 'ADMIN' || classRole === 'HEAD' || classRole === 'SUBJECT' || classRole === 'CLASS_DEVICE'
);
}
/**
@@ -164,7 +166,9 @@ export const PERMISSION_KEYS = [
// 班级与人员
'class:read',
'class:manage',
'roster:read',
'roster:manage',
'teacher:read',
'teacher:manage',
'schedule:read',
'schedule:write',
@@ -185,8 +189,6 @@ export const PERMISSION_KEYS = [
'import:schedule',
'import:class-teachers',
// 设备与联动
'device:manage',
'classisland:notify',
'client:read',
'client:manage',
'client:update-push',
@@ -204,8 +206,6 @@ export const PERMISSION_KEYS = [
'ai:manage',
'permission:manage',
'system:manage',
'log:read',
'developer:mode',
] as const;
export type PermissionKey = (typeof PERMISSION_KEYS)[number];
@@ -214,7 +214,9 @@ export type PermissionKey = (typeof PERMISSION_KEYS)[number];
export const PERMISSION_LABELS: Record<PermissionKey, string> = {
'class:read': '查看班级与班级列表',
'class:manage': '创建 / 修改 / 删除班级(含班级账号密码)',
'roster:read': '查看学生名单',
'roster:manage': '学生名单管理(增删 / 调班 / 导入)',
'teacher:read': '查看教师名单',
'teacher:manage': '教师账号管理(新建 / 导入 / 重置密码 / 删除)',
'schedule:read': '查看课表',
'schedule:write': '课表与课程编辑',
@@ -232,8 +234,6 @@ export const PERMISSION_LABELS: Record<PermissionKey, string> = {
'import:teachers': '导入教师名单',
'import:schedule': '导入课表时间配置',
'import:class-teachers': '导入班级任课关系',
'device:manage': '联动设备管理(ClassIsland 设备令牌 / 课表上报)',
'classisland:notify': '向 ClassIsland 下发提醒',
'client:read': '查看 ClassHelper 客户端在线状态与版本',
'client:manage': '管理客户端设备(改名 / 停用 / 删除)',
'client:update-push': '向客户端统一下发更新',
@@ -249,15 +249,22 @@ export const PERMISSION_LABELS: Record<PermissionKey, string> = {
'ai:manage': '大模型设置(开关 / 服务地址 / 密钥 / 模型)',
'permission:manage': '权限组与用户权限管理',
'system:manage': '全局系统设置(含开发者模式)',
'log:read': '查看服务端日志',
'developer:mode': '开发者模式',
};
/** 权限组管理页的分组展示(只影响排序与标题,不改变语义) */
export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly PermissionKey[] }[] = [
{
title: '班级与人员',
keys: ['class:read', 'class:manage', 'roster:manage', 'teacher:manage', 'schedule:read', 'schedule:write'],
keys: [
'class:read',
'class:manage',
'roster:read',
'roster:manage',
'teacher:read',
'teacher:manage',
'schedule:read',
'schedule:write',
],
},
{
title: '内容发布',
@@ -284,8 +291,8 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
],
},
{
title: '设备与联动',
keys: ['device:manage', 'classisland:notify', 'client:read', 'client:manage', 'client:update-push', 'remote:control'],
title: '客户端设备',
keys: ['client:read', 'client:manage', 'client:update-push', 'remote:control'],
},
{ title: '学期与统计', keys: ['term:read', 'term:write', 'archive:manage', 'dashboard:read'] },
{
@@ -298,8 +305,6 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
'ai:manage',
'permission:manage',
'system:manage',
'log:read',
'developer:mode',
],
},
];
@@ -307,7 +312,7 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
/**
* 每个权限键的**默认角色**(镜像改造前各路由的 `requireRole(...)`,逐条对齐)。
*
* 这是"没有分过任何权限组"时的回落模板:行为与权限组功能上线前完全一致。
* 这是创建角色默认组时的初始模板;默认组写入数据库后允许自定义。
* 一旦管理员给某个账号分了组,生效集合就**只由组的并集决定**(可以比默认更少)。
*
* 注意 `CLASS_DEVICE` 不出现在任何默认里:班级端的能力由班级维度断言
@@ -316,7 +321,9 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
export const PERMISSION_DEFAULT_ROLES: Record<PermissionKey, readonly UserRole[]> = {
'class:read': ['ADMIN', 'TEACHER'],
'class:manage': ['ADMIN'],
'roster:read': ['ADMIN', 'TEACHER'],
'roster:manage': ['ADMIN', 'TEACHER'],
'teacher:read': ['ADMIN', 'TEACHER'],
'teacher:manage': ['ADMIN'],
'schedule:read': ['ADMIN', 'TEACHER'],
'schedule:write': ['ADMIN', 'TEACHER'],
@@ -334,8 +341,6 @@ export const PERMISSION_DEFAULT_ROLES: Record<PermissionKey, readonly UserRole[]
'import:teachers': ['ADMIN'],
'import:schedule': ['ADMIN', 'TEACHER'],
'import:class-teachers': ['ADMIN'],
'device:manage': ['ADMIN', 'TEACHER'],
'classisland:notify': ['ADMIN', 'TEACHER'],
'client:read': ['ADMIN', 'TEACHER'],
'client:manage': ['ADMIN'],
'client:update-push': ['ADMIN'],
@@ -351,11 +356,9 @@ export const PERMISSION_DEFAULT_ROLES: Record<PermissionKey, readonly UserRole[]
'ai:manage': ['ADMIN'],
'permission:manage': ['ADMIN'],
'system:manage': ['ADMIN'],
'log:read': ['ADMIN'],
'developer:mode': ['ADMIN'],
};
/** 全部权限键(管理员的生效集合;也用在校验 zod 的枚举上) */
/** 全部权限键(默认组初始模板;也用在校验 zod 的枚举上) */
export const ALL_PERMISSION_KEYS: readonly PermissionKey[] = PERMISSION_KEYS;
/**
@@ -375,7 +378,10 @@ export function defaultPermissionsForRole(role: UserRole): PermissionKey[] {
}
/** 判断权限集合里是否有某个键(`Set` 或数组都接受) */
export function hasPermission(permissions: ReadonlySet<string> | readonly string[], key: PermissionKey): boolean {
export function hasPermission(
permissions: ReadonlySet<string> | readonly string[],
key: PermissionKey,
): boolean {
const list: readonly string[] = Array.isArray(permissions) ? permissions : [...permissions];
return list.includes(key);
}
+16 -370
View File
@@ -405,10 +405,6 @@ export interface ClassDto {
termWeeks: number;
/** 本班学期开始日期(YYYY-MM-DD);null = 用全局配置 */
termStartDate?: string | null;
/** 是否已有启用中的 ClassIsland 联动设备(班级列表/课表页显示「已接入」徽标) */
classIslandConnected?: boolean;
/** 通知显示位置(由 ClassHelper 班级端设置,见 CLASSISLAND_NOTIFICATION_CHANNELS) */
notificationChannel?: ClassIslandNotificationChannel;
/**
* 是否允许 ClassHelper 班级端按学号查询本班学生的成绩明细(默认开启)。
* 关闭后班级端查成绩一律 403,教师端不受影响。
@@ -1185,6 +1181,7 @@ export interface DashboardSummary {
/** 服务端 -> 客户端事件名 */
export interface ServerToClientEvents {
'notification:deleted': (payload: { id: string; classId: string }) => void;
'notification:new': (payload: NotificationDto) => void;
/** 叫人:定向广播给被叫学生 */
'call:new': (payload: NotificationDto) => void;
@@ -1195,16 +1192,11 @@ export interface ServerToClientEvents {
'grade:updated': (payload: GradeDto) => void;
'schedule:updated': (payload: {
classId: string;
/** imported = 由 ClassIsland 插件上报后整体刷新课表 */
/** imported = 导入或整体刷新课表 */
action: 'created' | 'updated' | 'deleted' | 'imported';
schedule?: ScheduleDto;
}) => void;
'class:updated': (payload: { classId: string; action: 'created' | 'updated' | 'deleted' }) => void;
/** ClassIsland 联动:设备上报了当前上课状态(Web 端实时展示「现在上什么课」) */
'classisland:state': (payload: ClassIslandStateEvent) => void;
/** ClassIsland 联动:老师发的提醒已下发给该班设备(Web 端据此提示「已送达」) */
'classisland:notification': (payload: ClassIslandNotificationEvent) => void;
/** 会话主体 id:教师/管理员是自己的账号 id,ClassHelper 班级端是班级 id */
connected: (payload: { sessionId: string; role: UserRole; rooms: string[] }) => void;
}
@@ -1288,365 +1280,6 @@ export interface TimeLayoutImportResult {
replaced: number;
}
/* ------------------------------------------------------------------ ClassIsland 联动(集成) */
/**
* 通知的显示位置(存在班级上,由教室的班级客户端在设置页里选)。
* 服务端据此决定「发通知时要不要同时推给教室的 ClassIsland」。
*/
export type ClassIslandNotificationChannel = 'both' | 'client' | 'classisland';
/**
* 联动方式:
* - `plugin`:安装「ClassHelper 联动插件」,由插件把 ClassIsland 的当前状态与课表推给本服务;
* - `import`:只把 ClassIsland 导出的档案 JSON 导入本服务(一次性,无实时联动)。
*/
export type IntegrationMode = 'plugin' | 'import';
/** 设备(一台安装了 ClassIsland 插件的机器)与班级的绑定关系 */
export interface IntegrationDeviceDto {
id: string;
classId: string;
className?: string | null;
/** 设备显示名(默认取机器名) */
name: string;
/** 设备唯一标识(插件上报的机器码) */
deviceKey: string;
/** ClassIsland 版本,例如 2.1.0.0 */
classIslandVersion: string | null;
/** 插件版本 */
pluginVersion: string | null;
mode: IntegrationMode;
/** 是否接入:接入后插件才能上报状态/课表并接收通知 */
enabled: boolean;
/**
* 是否允许插件把 ClassIsland 的课表**自动**回传到本服务(覆盖/合并本班课表)。
* **默认关闭**:课表以服务端为准,自动回传会在老师手排课之后被教室的旧课表覆盖;
* 要取教室的课表请用「从教室机器获取课表」这个手动动作。
*/
syncScheduleToServer: boolean;
/** 是否允许把班级课表镜像回 ClassIsland(由插件执行) */
mirrorScheduleToClassIsland: boolean;
/** 最后一次"带运行状态"的上报时间(心跳之外还有实际内容的那次) */
lastSeenAt: string | null;
/** 最后一次心跳时间(每次上报都刷新);在线判定看它 */
lastHeartbeatAt: string | null;
/** 是否在线:最后一次心跳在 CLASS_HELPER_ONLINE_WINDOW_MS 之内 */
online: boolean;
/** 最后一次上报里 ClassIsland 是否已加载课表 */
classPlanLoaded: boolean;
/** 最后一次上报里的当前科目 */
currentSubject: string | null;
/** 最后一次上报里的当前时间点状态(OnClass / Breaking / AfterSchool / None / PrepareOnClass) */
currentTimeState: string | null;
/** 最后一次上报里的当前节次起止时间(HH:mm) */
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
/** 最后一次上报里的下一节课科目 */
nextSubject: string | null;
/** 是否有待办的「立即上报一次课表」请求(Web 端点过之后置位,插件上报后清除) */
requestScheduleReport: boolean;
/** 令牌前缀提示(chci_xxxxxxxx…):设备列表里用于区分多台设备各自用的令牌 */
tokenHint?: string;
/** 创建/重置接口一次性下发的纯文本设备令牌(其他接口永不返回) */
token?: string;
createdAt: string;
updatedAt: string;
}
/** 创建/重置设备令牌的返回(token 只在这一次返回) */
export interface IntegrationDeviceTokenDto {
device: IntegrationDeviceDto;
token: string;
}
export interface CreateIntegrationDeviceRequest {
classId: string;
name?: string;
mode?: IntegrationMode;
syncScheduleToServer?: boolean;
mirrorScheduleToClassIsland?: boolean;
}
export interface UpdateIntegrationDeviceRequest {
name?: string;
enabled?: boolean;
syncScheduleToServer?: boolean;
mirrorScheduleToClassIsland?: boolean;
mode?: IntegrationMode;
}
/* ------------------ ClassHelper 班级端在线状态(教师网页查看) ------------------ */
/** 一台 ClassHelper 班级端设备的在线状态 */
export interface ClassHelperDeviceStatusDto {
deviceId: string;
/** 设备显示名(默认取机器名) */
deviceName: string;
/** 设备/机器码 */
deviceKey: string;
/** 是否接入(停用的设备一律按离线展示) */
enabled: boolean;
/** 在线 = 最近一次心跳在 CLASS_HELPER_ONLINE_WINDOW_MS 之内 */
online: boolean;
/** 最后一次带运行状态的上报时间 */
lastSeenAt: string | null;
/** 最后一次心跳时间 */
lastHeartbeatAt: string | null;
pluginVersion: string | null;
classIslandVersion: string | null;
}
/**
* 某个班的 ClassHelper 班级端在线状态。
*
* 范围:管理员看全部班级;普通教师看自己担任班主任的班级 ∪ 任课(有 Course)的班级。
*/
export interface ClassHelperStatusDto {
classId: string;
className: string;
grade: string;
/** 该班绑定的全部设备(可能多台:讲台机 + 备用机) */
devices: ClassHelperDeviceStatusDto[];
/** 任一设备在线即视为该班在线 */
online: boolean;
/** 是否允许班级端按学号查询本班学生成绩明细 */
studentGradeQueryEnabled: boolean;
}
/** GET /api/integrations/classisland/status 的返回 */
export interface ClassHelperStatusListDto {
/** 后端判定所用的服务器时间(前端据此算"距今多久",避免依赖本机时钟) */
serverTime: string;
/** 在线判定窗口(毫秒) */
onlineWindowMs: number;
classes: ClassHelperStatusDto[];
}
/** 通知下发:把一条通知推给某班级的 ClassIsland 设备(在 ClassIsland 上全屏提醒) */
export interface SendClassIslandNotificationRequest {
classId: string;
title: string;
content: string;
/** 显示时长(秒,1~120,默认 8):映射到 ClassIsland 的 NotificationContent.Duration */
durationSeconds?: number;
/** 是否语音朗读(默认 false) */
speech?: boolean;
/** 语音朗读内容(留空则朗读 title + content) */
speechContent?: string;
/** 是否同时落库到班级通知中心(默认 true) */
saveToNotifications?: boolean;
/** 优先级(落库时使用,默认 NORMAL) */
priority?: NotificationPriority;
}
export interface SendClassIslandNotificationResult {
/** 已下发(在线)的设备数 */
delivered: number;
/** 目标设备总数 */
targetCount: number;
/** 一并落库的通知 id(saveToNotifications=false 时为 null) */
notificationId: string | null;
/**
* 本次被跳过的原因(没有跳过时为 null/undefined)。
* `channel-client` = 该班教室在客户端里选了"只在 ClassHelper 客户端显示",
* 因此没有推送到 ClassIsland —— 让老师知道"不是没送达,是教室没要"。
*/
skipped?: 'channel-client' | null;
}
/* ------------------ 插件上报(设备令牌鉴权,不是 JWT 会话) ------------------ */
/** 插件上报的课表条目(已按本系统口径归一化) */
export interface ClassIslandScheduleEntry {
/** 1=周一 … 7=周日(插件已从 ClassIsland 的 0=周日 转换) */
dayOfWeek: number;
startTime: string;
endTime: string;
subject: string;
teacherName?: string | null;
weekParity: WeekParity;
weekCountDiv: number;
weekCountDivTotal: number;
planName: string;
}
/** 插件上报的节次时间(时间表) */
export interface ClassIslandTimeLayoutEntry {
index: number;
name: string;
startTime: string;
endTime: string;
type: 'class' | 'break' | 'divider' | 'action';
skipped: boolean;
}
/** POST /api/integrations/classisland/report —— 插件的一次上报 */
export interface ClassIslandReportRequest {
/** 插件/应用版本,便于排查 */
pluginVersion?: string;
classIslandVersion?: string;
/** 本机运行状态 */
state?: {
inClass: boolean;
subject: string | null;
nextSubject: string | null;
/** 当前时间点状态字符串(ClassIsland TimeState) */
timeState: string | null;
periodStart: string | null;
periodEnd: string | null;
week: number | null;
classPlanLoaded: boolean;
/** 客户端本机时间,用于诊断时钟偏差 */
clientTime: string;
};
/** 课表(仅在 syncScheduleToServer 打开时有意义;不传表示本次不上报课表) */
schedule?: {
/** replace 覆盖本班课表 / merge 合并(默认) */
mode: 'replace' | 'merge';
entries: ClassIslandScheduleEntry[];
} | null;
/** 节次时间表 */
timeLayout?: {
name?: string;
mode: 'replace' | 'merge';
items: ClassIslandTimeLayoutEntry[];
} | null;
}
/** POST /api/integrations/classisland/report 的返回 */
export interface ClassIslandReportResult {
/** 服务端是否接受并应用了课表 */
scheduleApplied: boolean;
scheduleCreated: number;
scheduleUpdated: number;
/** 自动补建/命中的课程名 */
courses: string[];
timeLayoutApplied: boolean;
/** 服务端当前教学周(插件可用来校正"第几周") */
week: number;
/** 服务端时间(插件可用来校正时钟) */
serverTime: string;
/** 回传给插件的外观/行为设置:本轮需要下发的通知已随实时事件推送 */
settings: {
mirrorScheduleToClassIsland: boolean;
enabled: boolean;
/**
* 服务端请求插件**立刻上报一次课表**(Web 端点了「从教室机器获取课表」)。
* 两个课表方向都只由这类一次性待办触发,本机没有"自动回传 / 自动镜像"的开关。
*/
scheduleRequested?: boolean;
/**
* 服务端请求插件**下发一次课表到教室**(Web 端点了「下发课表到教室」)。
* 插件看到就去拉本班课表并**强制替换**写进 ClassIsland(停用同一天同一单双周相位下
* 冲突的旧课表,只停用不删除);拉取成功时由服务端清除这个待办。
*/
mirrorRequested?: boolean;
};
/** 尚未在 ClassIsland 上确认过的一条提醒(插件可立即弹出) */
pendingNotification?: ClassIslandPushNotification | null;
}
/** 下发给 ClassIsland 的提醒(教师发通知 → 学生机器全屏弹出) */
export interface ClassIslandPushNotification {
id: string;
title: string;
content: string;
/** 显示时长(秒) */
durationSeconds: number;
/** 语音朗读内容;null = 不朗读 */
speechContent: string | null;
createdAt: string;
/** 是否紧急(紧急时插件用更强的遮罩与更长的时长) */
urgent: boolean;
/**
* 提醒类型:
* - `notification`:通知类提醒 —— **上课时段插件会暂存,下课后才弹**;
* - `call`:叫人等「主动通知」—— 不论是否上课都立刻弹(老师正在等学生)。
*/
kind: 'notification' | 'call';
classId: string;
className?: string | null;
teacherName?: string | null;
}
/** GET /api/integrations/classisland/pending 的返回(插件重连后补齐漏掉的通知) */
export interface ClassIslandPendingResult {
notifications: ClassIslandPushNotification[];
serverTime: string;
week: number;
}
/** 插件拉取本班的整份课表(mirrorScheduleToClassIsland 打开时,插件把课表写回 ClassIsland) */
export interface ClassIslandClassPlanPull {
/** ClassIsland 的口径:0=周日、1=周一 … 6=周六 */
classPlan: {
entries: {
/** ClassIsland 的 WeekDay:0=周日 … 6=周六 */
weekDay: number;
weekCountDiv: number;
weekCountDivTotal: number;
/** 科目名 */
subject: string;
teacherName: string | null;
/**
* 该课目的起止时间(HH:mm)。
*
* ClassIsland 的 `ClassPlan.Classes[i]` 必须与时间表里第 i 个上课点对齐,
* 因此插件要按这些时间重建时间表 —— 缺了它们整份镜像课表都会错位。
*/
startTime: string;
endTime: string;
timeLayoutId: string;
}[];
timeLayouts: {
id: string;
name: string;
layouts: {
startTime: string;
endTime: string;
/** ClassIsland TimeType:0=上课、1=课间、2=分割线、3=行动 */
timeType: number;
}[];
}[];
/** 建议的档案名 */
profileName: string;
/** 教学周起始日期(用于插件设置 ClassIsland 的周次口径) */
termStartDate: string;
};
week: number;
/**
* 学期逐周区间(管理员在「学期周次」里配的)。
* 空数组 = 完全按 `termStartDate` 线性推算。
* ClassIsland 自身只认"教学周序号 + 单双周",因此它拿这个主要是为了
* 校正"现在是第几周"、以及在设置页里展示教室所在的实际日期区间。
*/
weekRanges?: Array<{ weekNumber: number; startDate: string; endDate: string }>;
serverTime: string;
}
/** Socket.IO 事件:ClassIsland 设备状态变化(Web 管理端实时展示) */
export interface ClassIslandStateEvent {
classId: string;
deviceId: string;
deviceName: string;
inClass: boolean;
subject: string | null;
nextSubject: string | null;
timeState: string | null;
periodStart: string | null;
periodEnd: string | null;
week: number | null;
classPlanLoaded: boolean;
at: string;
}
/** Socket.IO 事件:有新提醒下发到 ClassIsland(插件与 Web 端都可据此刷新) */
export interface ClassIslandNotificationEvent extends ClassIslandPushNotification {
/** 是下发给本班级设备,还是仅通知 Web 端展示 */
targetCount: number;
}
/** 客户端 -> 服务端事件名 */
export interface ClientToServerEvents {
'class:join': (classId: string, ack?: (result: { ok: boolean; message?: string }) => void) => void;
@@ -1755,7 +1388,7 @@ export interface UpdateAssetDto {
*
* 两点约定:
* 1. **`ok:false` 是正常结果,不是异常** —— 服务器没有外网、GitHub 限流、请求超时都会走到这里,
* 调用方按"暂时查不到"呈现即可(与 ClassIsland 推送失败只记日志是同一种处理思路)。
* 调用方按"暂时查不到"呈现即可。
* 2. **`currentVersion` 由调用方各自提供**:服务端比的是服务端版本、客户端比的是客户端版本。
* 两边安装版本可能不同(例如教室机没跟着升级),所以不能由一端替另一端判断。
*/
@@ -1796,6 +1429,7 @@ export interface PermissionGroupDto {
memberCount: number;
/** 内置组(种子创建)不可删除,只允许改成员与键 */
builtin: boolean;
defaultRole?: UserRole | null;
createdAt: string;
updatedAt: string;
}
@@ -1860,6 +1494,8 @@ export interface MyPermissionsDto {
/* ------------------------------------- 客户端设备 / 远控 / 客户端更新 */
/** 远控命令种类(与 shared 的常量 `CLIENT_COMMAND_KINDS` 一一对应) */
export type ClientUpdateMode = 'idle' | 'immediate' | 'after-school';
export type ClientCommandKind = 'shutdown' | 'restart' | 'logoff' | 'lock';
/** 一条下发给教室客户端电脑的命令 */
@@ -1891,6 +1527,8 @@ export interface ClientDeviceDto {
autoUpdate: boolean;
/** 服务端指定的目标版本;`''` = 不指定 */
updateTargetVersion: string;
updateMode: ClientUpdateMode;
updateNotBefore: string | null;
/** 已下载待安装的版本;`''` = 无 */
pendingVersion: string;
lastHeartbeatAt: string | null;
@@ -1915,10 +1553,13 @@ export interface ClientDeviceDto {
/** `POST /api/clients/heartbeat`:客户端每 30 秒一次的上报 */
export interface ClientHeartbeatRequest {
/** 旧版缺省为 1;第二版正确解析统一响应并支持安装时机。 */
agentProtocol?: number;
deviceKey: string;
name?: string;
clientVersion?: string;
platform?: string;
autoUpdate?: boolean;
pendingVersion?: string | null;
stateNote?: string;
}
@@ -1940,6 +1581,8 @@ export interface ClientHeartbeatResponse {
serverTime: string;
/** 服务端要求升到的版本;null = 不要求 */
updateTargetVersion: string | null;
updateMode: ClientUpdateMode;
updateNotBefore: string | null;
/** 目标版本的安装包信息;null = 服务端还没有这份安装包(客户端只能提示手动更新) */
update: ClientUpdateInfoDto | null;
/** 待执行命令(最早的一条);null = 无 */
@@ -1988,6 +1631,9 @@ export interface PushClientUpdateRequest {
version?: string | null;
/** true = 取消下发(回到"不要求更新"的状态) */
clear?: boolean;
mode?: ClientUpdateMode;
/** 放学后策略的兜底时间;有课表时取最后一节课结束与此时间的较晚者 */
afterSchoolTime?: string;
}
/** `GET /api/update/client/releases`:服务端已缓存的客户端安装包 */
+1 -1
View File
@@ -99,7 +99,7 @@ export function weekParityOf(week: number): Exclude<WeekParity, 'ALL'> {
* `WeekCountDiv <= 0` 或 `Total <= 1` 表示每周都上。
*
* 注意:本系统只用单双周(2 周循环),3 周及以上的循环**无法表达**,此时降级为「每周」
* 并由调用方向用户提示(导入模块记 warning)。这里集中为一处,避免导入与插件上报各写一套。
* 并由调用方向用户提示(导入模块记 warning)。这里集中为一处,保持导入口径一致。
*/
export function weekParityFromDiv(weekCountDiv: number, weekCountDivTotal: number): WeekParity {
if (weekCountDiv <= 0 || weekCountDivTotal <= 1) return 'ALL';