项目架构
项目架构
RemoteCI 采用一个主项目仓库和一个独立文档仓库。当前稳定三端软件版本统一为 3.2.1.4,通信协议号独立为整数 3;稳定版使用 ClassIsland 要求的四段纯数字标签,Beta 保留 v3.x.x-beta.y 供测试且不进入插件市场。协议 v3 拆分主界面与电源权限,并增加独立扩展权限、逐扩展策略和能力协商。
运行组件
ClassIsland 插件 ── 局域网 WebSocket(HMAC 挑战认证)── Wear OS
│ │
└──────────── 云端 WebSocket ────────────┘
│
RemoteCI 服务端
账号 / 权限 / 状态 / 课表 / 命令中转| 目录 | 技术 | 职责 |
|---|---|---|
plugin | .NET 8、ClassIsland Plugin SDK | 收集课表状态、提供局域网服务、执行远程命令、注册扩展、旁路观察通知 |
server | ASP.NET Core 10、SQLite、WebSocket | 身份认证、权限检查、状态保存、消息中转与自动更新 |
wearos | Kotlin、Compose for Wear OS(minSdk 30) | 展示课程、课表、通知与操作界面 |
android | Kotlin / Jetpack Compose Material 3 Expressive 手机应用(minSdk 26)。 | 承接 WebUI 与手表的课表、控制和人员管理 |
shared | C# 模型库 | 插件与服务端共享协议 v3 和数据模型 |
数据流
- 插件从 ClassIsland 收集状态与七日课表,状态按秒推送,课表独立低频同步。
- 插件把数据推送给局域网手表和云端服务端;服务端保存最近状态并转发给已认证手表。插件每次云端连接还会上报本机候选 IP、局域网端口与服务状态,服务端缓存并同步给手表用于自动直连。
- 手表提交命令时,服务端先检查账号权限,再把命令转发给在线插件。
- 插件执行后返回真实回执(成功、失败码、新修订号),服务端再把结果传回发起方,等待上限 15 秒。
- 插件旁路观察 ClassIsland 统一通知入口,把自动化“显示提醒”和第三方插件通知转换为事件广播;其他插件注册的扩展功能清单通过
extensions_sync同步给服务端和手表,WebUI 从服务端缓存渲染同一套控制入口。云端服务端与插件局域网服务都会缓存最近一次扩展清单,并在手表认证后补发。 - 插件在云端连接建立后通过
software_inventory上报设备名称、ClassIsland 与插件版本;服务端按插件凭据保存最后快照。批量控制页通过DeviceInventoryService把班级/分组和具体设备选择合并成定向投递列表,再下发插件管理、软件更新、集控加入和策略命令;独立档案页复用同一目标选择服务下发档案。 - 首次登录可用 UDP
48765扫描插件;选中后通过未登录的/bootstrapWebSocket 仅读取云服务器地址。密码仍通过云端 HTTPS 登录,设备会话同步给插件后再进行局域网 HMAC 认证。
协议 v3 消息
所有 WebSocket 信封都带 protocolVersion: 3、type、messageId 与可选 replyToMessageId。软件版本不参与连接拒绝,协议号为 3 的端均可通信;协议号不是 3 才会被明确拒绝。主要类型:
| 类型 | 方向 | 用途 |
|---|---|---|
auth_challenge / auth_proof | 插件 ↔ 手表 | 局域网一次性 HMAC 挑战认证 |
account_sync | 服务端 → 插件 | 账号、权限、设备验证器、服务端版本与可选服务端能力镜像 |
peer_capabilities | 插件/手表 → 服务端 | 软件版本与稳定字符串能力列表 |
capabilities_sync | 服务端/插件 → 手表 | 服务端能力与接收方每个可访问班级主插件的能力(classPlugins);客户端按当前班级取用 |
software_inventory | 插件 → 服务端 | 设备、ClassIsland 与插件版本清单及最近升级状态 |
state_push | 插件 → 服务端/手表 | 高频当前课程状态 |
schedule_sync | 插件 → 服务端/手表 | 未来七日课表 |
schedule_pull | 服务端/手表 → 插件 | 请求插件立即重新生成并推送七日课表;不携带参数 |
extensions_sync | 插件 → 服务端/手表 | 扩展功能清单;服务端同时供 WebUI 控制菜单读取 |
extension_groups_sync | 插件 → 服务端 | 扩展分组(插件)、设置字段与设备当前设置值;只供 WebUI 控制页、批量控制页和扩展插件设置页使用 |
event_notify | 插件 → 服务端/手表 | 上课、课间、放学、课表变更、自定义、自动化与插件通知事件 |
plugin_network_info | 插件 → 服务端 → 手表 | 可用局域网 IP、监听端口与局域网服务状态;服务端缓存最近一次结果 |
connection_bootstrap | 插件 → 手表 | 用户选中局域网插件后提供电脑名称与云服务器地址;不携带认证数据 |
command / command_result | 手表/服务端 → 插件 | 换课、通知、清除提醒、主界面、电源、音量、扩展、软件升级、插件管理、档案分发、时间表、集控、科目教师、远程终端与文件分发命令 |
命令编号:1 换课、2 发送通知、3 清除提醒、4 主界面显隐、5 电源、6 音量、7 运行扩展、8(旧版协议保留)、9 发送语音、10 升级插件、11 升级 ClassIsland、12 刷新软件版本清单、13 安装插件、14 卸载插件、15 启用/禁用插件、16 设置远程插件管理策略、17 分发档案、18 更新时间表、19 加入集控、20 仅重启 ClassIsland、21 设置科目教师、22 远程终端、23 文件分发、24 修改扩展插件设置(只能由服务端 WebUI 或扩展设置 API 发起,局域网直连拒绝)、25 ApplyProfile 档案应用、26 ReadProfile 读取设备档案(两者只能由服务端档案页或档案接口发起)。事件编号:1 上课、2 课间、3 放学、4 课表变更、5 自定义、6 自动化通知、7 第三方插件通知。
SendVoiceMessage 的 voiceMessage 包含固定格式 format: pcm_s16le_16000_mono 和标准 Base64 的 audioBase64;录音为 16 kHz、16 位小端单声道 PCM,最多 60 秒(1,920,000 字节)。WebUI 使用带防伪头的二进制上传,服务端转成同一命令;手表使用已认证的云端或局域网 WebSocket。传输接收上限为 16 MiB,以容纳 JSON 转义的最坏情况,音频解码后仍执行严格大小校验。各接入端覆盖发送人,插件只使用认证身份。语音不持久化、不离线排队,已有浮窗返回 BUSY。Windows 播放器使用 NAudio/WASAPI;Avalonia 浮窗复用宿主 popup-bg、TransparentButton、原生 Slider 和 FluentIcon,并在 Windows 上检测浮窗外的鼠标按下以实现不抢焦点的轻触关闭;通知开启强调,关闭提示音和朗读。
插件完成云端认证后,服务端立即发送一次 schedule_pull,以消除“插件启动时已生成课表、但 WebSocket 尚未连上”的竞态。已认证手表和手机也可发出同一只读消息:云端由服务端转发,但只接受系统管理员或班级自治策略允许的目标班级班主任的请求,其他账号的请求会被忽略;局域网由插件直接处理。该消息与换课命令分离,不需要也不会授予管理课表权限。
服务端档案库
档案管理由独立档案服务与 Razor Page handlers 承担,不在 BatchControlModel 中扩展。管理区 /Profiles 提供全局模板库和批量编辑;班级菜单 /ClassProfiles 固定当前班级。“控制”和“批量控制”保留集控加入、插件策略等设备操作;“课表”页继续处理七日课表查看、换课与科目教师设置。
SQLite/EF Core 保存完整 ClassIsland JSON、名称、修订号、来源模板和更新时间,每班至多一份工作副本。分配模板复制 JSON 并保留对象 ID,各班与模板后续修改相互隔离。服务端不依赖 ClassIsland UI 程序集,按 JSON 节点编辑已支持的字段,保留未知字段及附加配置;上传上限 5 MB,解析预览后确认保存。时间表上课时段变化会按原时段保留关联课程,新时段插入空课程;删除已排课程的时段需确认。无效时间和缺失引用阻止保存、下发。
草稿保留在浏览器,切换班级不丢弃;统一保存先验证全部草稿与预期修订号,再在一个数据库事务提交,任一冲突整批不写入。服务器保存与设备下发分开,离线可保存,发送使用已保存且修订号匹配的版本。全局模板仅系统管理员可访问;班级档案要求系统管理员,或目标班级成员身份为班主任且拥有 ManageSchedule。上传、保存、导出、删除和下发均重新鉴权。
新增命令 25 ApplyProfile 与非基础能力 profile.apply。profileApply 载荷为 ProfileApplyRequest { profileJson, sections, mode, importProfileName?, restartAfter, replaceExistingTempLayers },sections 为位掩码(1 时间表、2 课表、4 科目),mode 为 1 MergeCurrent、2 ReplaceSections、3 CreateAndActivate 或 4 TempLayers(另需能力 profile.temp-layer),每次发送必须明确选择。前两种更新当前档案,分别保留其他内容或清空所选类别;创建模式要求设备档案名,重名报错。选中对象的必要依赖随档案一起下发,合并后的悬空引用使操作失败。
命令 26 ReadProfile(能力 profile.read)无载荷,插件在 UI 线程把当前内存档案序列化后放进 CommandResult.data 返回;服务端逐班校验权限后并发发送,按宿主 RefreshClassesList 规则对齐课程数并清除悬空指针,结果只作为浏览器草稿或 API 返回值,不写入档案库。
临时层即 IsOverlay=true 的课表(及可选的临时层时间表),由 OrderedSchedules[日期] 指向。共享层 ProfileDocument 让常规选择跳过临时层,更新与整体替换后按宿主 RefreshClassesList 规则修复设备临时层(对齐课程数、缺失科目改空课、缺失课表群改默认),仅时间表不存在时移除;BuildTempLayerSelection 只打包所选临时层及其依赖,ApplyTempLayers 在插件端逐日写入:跳过过期日期,同日已有安排需明确替换,科目与课表群只补缺,时间表与设备不一致时写成临时层时间表副本,临时层课表用新 ID;写入后总是打开 IsOverlayClassPlanEnabled(宿主只有该开关打开才按日期使用临时层),当天生效时设置 OverlayClassPlanId。编辑器按各班状态快照的时区偏移判断临时层是否过期。
ApplyProfile 与 ReadProfile 属于 serverOnly,仅档案页或档案接口构造;通用 REST 命令、手机/手表 WebSocket 与插件局域网直连拒绝。服务端复核班级身份、权限、目标所属班级与能力;插件在 UI 线程验证、应用、保存,失败回滚,成功后立即重新同步七日课表。班级级目标沿用最早接入的在线设备,具体设备逐台投递;各班不同内容按目标班级组装,离线及失败逐台返回且不排队。旧命令 17、18 的语义和管理员约束保留,不能用于回退模拟新命令;旧插件可以编辑服务器档案,下发需升级。
档案库与班级副本纳入 schema 5 配置备份;旧备份缺少档案数据时按空库恢复。服务器不会自动读取、迁移或同步设备现有档案,设备本地修改也不会回写;只有用户主动“从设备收集”时才读取。
身份与权限
服务端使用 SQLite 持久化用户、PBKDF2 密码哈希、设备会话、插件凭据和一次性配对码。有效权限是位掩码:
| 值 | 权限 |
|---|---|
| 1 | 查看当前课程 |
| 2 | 概览 |
| 4 | 人员管理 |
| 8 | 发送与清除通知 |
| 16 | 换课 |
| 32 | 电源控制(含音量) |
| 64 | 保留权限位(暂时隐藏) |
| 128 | 扩展功能 |
| 256 | 主界面 |
| 512 | 发送语音 |
| 1024 | 修改用户名(仅为兼容旧权限数据保留) |
| 2048 | API 访问 |
管理员有效权限固定为 4095;普通用户固定含值 1,WebUI 课表查看只要求登录,课表拉取和自动拉取设置只允许系统管理员或当前班级班主任,值 16 保护换课与科目教师设置,值 512 独立保护发送语音,值 1024 仅为兼容旧权限数据保留、不再授予任何能力(用户名只能由系统管理员修改),值 2048 允许账号创建 API Key 并调用 REST API,默认不授予学生角色,班主任默认拥有。内置“老师”角色的账号按用户名与课表科目教师名动态关联任教班级,在任教班级内获得老师角色的默认权限;关联不落库,随课表缓存实时计算。API Key 不复制权限快照,每次请求都按账号当前角色与授权重新计算。访问令牌默认 1 小时,设备会话默认 30 天可续期;插件用一次性配对码换取长期凭据。扩展开放策略和个人手表展示偏好随授权镜像同步;镜像超过 24 小时未更新时,局域网直连只允许查看课程。
云端 WebSocket 只在握手时完整查询令牌,连接对象缓存插件凭据 ID、设备会话 ID、有效权限和访问令牌到期时间。普通 state_push、广播和命令转发不逐条访问 SQLite;凭据或设备会话吊销、改密、账号禁用和权限修改通过连接注册表主动失效,后台默认每分钟再做一次持久化授权兜底复查。
云端与局域网认证成功时,auth_state.serverVersion 都会携带当前 WebUI 软件版本;局域网值来自 account_sync.serverVersion。该版本用于诊断,不作为连接或手表更新上限。服务端和手表正式渠道只选择协议主版本相同的四段纯数字 Release,Beta 渠道额外选择 v3.x.x-beta.y及在 GitHub 标为预发布的四段 Release,V4 需要协调升级。
WebUI 的有效能力是“服务端 ∩ 当前主插件”,手表的有效能力是“手表本地 ∩ 服务端 ∩ 当前主插件”。未上报能力的旧 V3 端回退到 V3 基础能力(该基线自 3.1.0 引入),未知能力安全忽略。当前主插件为最早接入的健康插件;主插件切换或能力变化时重新广播快照。能力只控制入口和转发兼容性,账号权限与扩展策略仍独立校验。
版本与构建
新增 voice-message.send 必须显式协商,不加入旧 V3 端默认获得的基础能力。新端使用当前能力列表通告语音支持;界面能力与独立的发送语音权限同时满足才显示入口。
- 插件 3.2.1.4:net8.0,兼容 ClassIsland 2.x 宿主;插件程序集为 AnyCPU,可随 ClassIsland 的 x86、x64 与 arm64 Windows 宿主一起加载,因此不得设置 x64 等平台专用目标。CIPX 由
CreateCipx=true生成,市场资产固定为RemoteCI.Plugin.cipx。 - 插件读写宿主成员时区分两类 API:签名稳定的接口直接编译绑定;ClassIsland 2.2 预览版(2.1.1.x)改过签名的成员(
Profile.Subjects、Profile.ClassPlans、FluentIcon.Glyph、窗口装饰枚举、动态资源绑定的Bind重载)统一经HostApiCompat反射访问,避免同一份包在旧宿主可用、在新宿主抛MissingMethodException。注意同名重载差异(如Bind(AvaloniaProperty, IBinding)变为Bind(AvaloniaProperty, BindingBase))无法靠编译期检查发现,新增宿主调用后需按目标版本的签名实际核对。 - 服务端 3.2.1.4:net10.0,发布 linux-x64 / win-x64 平台包。
- 手表 3.2.1.4:minSdk 30 / targetSdk 37,Release APK 使用签名密钥构建。
- Wear OS 源码若位于云盘或 NAS 按需同步目录,可在不入库的
wearos/local.properties中设置remoteci.buildDir=C:/本地目录,将 Gradle 生成文件放到普通本地磁盘,避免重解析占位文件触发Cannot snapshot ... not a regular file;也支持环境变量REMOTECI_WEAROS_BUILD_DIR和命令行属性-Premoteci.buildDir。
开发状态
3.2.1.4 的独立提醒指令、权限、WebUI 控制页、V3 协议和三端能力协商已实现;当前 UI 入口暂时隐藏,协议和权限位保留用于旧客户端兼容。另已修复 ClassIsland x86/arm64 宿主的程序集架构加载与 2.2 预览版宿主 API 兼容问题。发布前仍应使用稳定四段版本和 Beta 版本在真机完成端到端验收,见开发状态。