用户反馈"叫人点不动"复盘:状态机层面没问题(真机点击日志能看到"收到用户动作 expand"), 问题在窗口/渲染层面。本轮修掉三个真实缺陷: 1) 幽灵胶囊:岛渲染进程一旦崩溃/卡死,Windows 会把最后一帧留在屏幕上 —— 岛还挂在最上方、 窗口也还在,但点击全部丢失,而客户端没有任何自愈机制(实测:崩掉渲染进程后点击不再产生 任何动作)。现在渲染进程每 5s 上报心跳,主进程 20s 收不到、或收到 render-process-gone / unresponsive 时,**先隐藏死窗口**再重建并重新下发状态与外观;10 分钟内最多重建 3 次, 超出则隐藏并告警,避免持续崩坏时无限重建。 2) 叫人双广播降级:服务端 createCall 同时发 notification:new(班级房间)与 call:new(user 房间), 两条 DTO 同 id,后到的那条会把"叫人"覆盖成普通通知 —— 胶囊显示成 "新消息 / 系统管理员 · 点击查看"(用户截图正是如此),紧急叫人还会被降级成"课中只排队、 不立即展开"。现在按 id 合并:类型 call 优先、优先级取更高(URGENT > HIGH > NORMAL)。 3) 淡出竞态:岛重新变为可见时没有取消进行中的淡出,那次淡出会继续把透明度降到 0 并 hide() 窗口,出现"状态是胶囊/展开、窗口却不可见"的错位(冒烟"岛外鼠标穿透"用例就是被它打红的)。 现在可见路径总是 cancelFade + 恢复透明度。 另加两条兜底: - 命中矩形兜底:渲染进程还没上报岛体矩形时,主进程按当前形态/停靠位置自己算一个, 杜绝"岛可见却一直不接收鼠标"。 - 形变兜底:实测打包版某些时刻(细缝态、刚启动)rAF 会被 Chromium 压到约 1 帧/秒, 此时"点击后形变"看起来就是没反应;现在形变开始 1.2s 后仍未收敛就直接 snap 到目标几何。 验证:pnpm verify:desktop 81/81(新增 3 条回归:双广播不降级 / 紧急叫人双广播 / 渲染进程崩溃后自愈;离线二次启动 73/73);打包版 verify:packaged 81/81、packaged=true、 二次启动 73/73,全绿。 顺带:细缝用例改为轮询等几何收敛(帧被限流时固定 sleep 会误判为"细缝尺寸不对"), 课表时间轴失败时打印各卡片状态(便于定位),并清理了库里 3 条遗留测试探针课表 (它们会污染客户端"今天"的课表)。
47 lines
2.0 KiB
TypeScript
47 lines
2.0 KiB
TypeScript
import { contextBridge, ipcRenderer } from 'electron';
|
|
import type { IslandAppearance, IslandState } from '@classhelper/shared';
|
|
|
|
/**
|
|
* 灵动岛窗口的预加载脚本。
|
|
* 只暴露「订阅状态/外观」与「发送用户操作」两件事,渲染进程不接触任何 Node 能力。
|
|
*/
|
|
export interface IslandBridge {
|
|
onState(handler: (state: IslandState) => void): void;
|
|
sendAction(action: 'expand' | 'collapse' | 'dismiss' | 'mark-read' | 'open-app', id?: string): void;
|
|
getState(): Promise<IslandState>;
|
|
/** 订阅个性化外观(设置页改动后实时生效) */
|
|
onAppearance(handler: (appearance: IslandAppearance) => void): void;
|
|
getAppearance(): Promise<IslandAppearance>;
|
|
/** 命中提示:指针在岛上 → 让窗口接收鼠标(只上报 true,关闭由主进程轮询决定) */
|
|
setInteractive(interactive: boolean): void;
|
|
/** 上报岛体矩形(窗口内 CSS px):主进程据此按光标位置兜底校正命中 */
|
|
setHitRect(rect: { x: number; y: number; width: number; height: number } | null): void;
|
|
/** 心跳:主进程据此发现"渲染进程卡死的幽灵窗口"(岛还在屏幕上但点不动)并重建窗口 */
|
|
alive(): void;
|
|
}
|
|
|
|
const bridge: IslandBridge = {
|
|
onState: (handler) => {
|
|
ipcRenderer.on('island:state', (_event, state: IslandState) => handler(state));
|
|
},
|
|
sendAction: (action, id) => {
|
|
ipcRenderer.send('island:action', { action, id });
|
|
},
|
|
getState: () => ipcRenderer.invoke('island:get-state'),
|
|
onAppearance: (handler) => {
|
|
ipcRenderer.on('island:appearance', (_event, appearance: IslandAppearance) => handler(appearance));
|
|
},
|
|
getAppearance: () => ipcRenderer.invoke('island:get-appearance'),
|
|
setInteractive: (interactive) => {
|
|
ipcRenderer.send('island:set-interactive', interactive);
|
|
},
|
|
setHitRect: (rect) => {
|
|
ipcRenderer.send('island:set-hit-rect', rect);
|
|
},
|
|
alive: () => {
|
|
ipcRenderer.send('island:alive');
|
|
},
|
|
};
|
|
|
|
contextBridge.exposeInMainWorld('island', bridge);
|