fix(island): 幽灵胶囊自愈 + 叫人双广播不降级 + 淡出/帧限流兜底(继续修"点不动")
用户反馈"叫人点不动"复盘:状态机层面没问题(真机点击日志能看到"收到用户动作 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 条遗留测试探针课表 (它们会污染客户端"今天"的课表)。
This commit is contained in:
1 parent
a7b0e9c2b3
commit
337d6eae53
6 files changed
+330
-11
No files matched your search
@@ -2077,13 +2077,35 @@ async function runIslandChecks(
|
||||
|
||||
// 10.8) 空闲细缝(参考 WinIsland hidden_width):开启后空闲留一条细缝,来消息再展开
|
||||
const sliver = getSliverSize();
|
||||
/**
|
||||
* 等岛体几何收敛到目标尺寸(不等固定时长)。
|
||||
* 原因:Windows 有时会把置顶透明小窗判定为"被遮挡/后台",Chromium 会把 rAF 压到 1 帧/秒
|
||||
* (实测打包版细缝态出现过 1.1fps),此时形变会明显变慢;渲染进程 1.2s 后会自动 snap 到目标,
|
||||
* 所以这里轮询等待即可,固定 sleep 会误判成"细缝尺寸不对"。
|
||||
*/
|
||||
const waitIslandSize = async (
|
||||
target: { width: number; height: number },
|
||||
timeoutMs = 4000,
|
||||
): Promise<{ width: number; height: number } | null> => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let last: { width: number; height: number } | null = null;
|
||||
while (Date.now() < deadline) {
|
||||
last = (await readIslandGeometry())?.island ?? null;
|
||||
if (last && Math.abs(last.width - target.width) <= 1 && Math.abs(last.height - target.height) <= 1) {
|
||||
return last;
|
||||
}
|
||||
await sleep(120);
|
||||
}
|
||||
return last;
|
||||
};
|
||||
island.setAppearance({ ...appearanceBefore, idleSliver: true });
|
||||
await drainIsland();
|
||||
await sleep(420);
|
||||
const sliverBounds = (await readIslandGeometry())?.island ?? null;
|
||||
const sliverBounds = await waitIslandSize(sliver);
|
||||
island.pushNotification(makeNotification('smoke-sliver', 'NORMAL', '空闲细缝校验'), { inClass: false });
|
||||
await sleep(700);
|
||||
const afterSliverNotification = (await readIslandGeometry())?.island ?? null;
|
||||
const afterSliverNotification = await waitIslandSize({
|
||||
width: island.getAppearance().width,
|
||||
height: island.getAppearance().height,
|
||||
});
|
||||
record(
|
||||
'空闲细缝:空闲缩为细缝、来消息自动展开(WinIsland hidden_width)',
|
||||
Math.abs((sliverBounds?.width ?? 0) - sliver.width) <= 1 &&
|
||||
@@ -2249,6 +2271,75 @@ async function runIslandChecks(
|
||||
island.setAppearance(appearanceBefore);
|
||||
await sleep(300);
|
||||
|
||||
// 4.12) 同一条消息的"双广播"不能把"叫人"降级成普通通知。
|
||||
// 服务端 createCall 既发 notification:new(班级房间)又发 call:new(user 房间),
|
||||
// 两条 DTO 同 id;后到的那条若直接覆盖,胶囊会显示成"新消息"(用户实测截图正是如此),
|
||||
// 紧急叫人还会被降级成"课中只排队、不立即展开"。
|
||||
const mergeCallId = 'smoke-call-merge';
|
||||
island.pushNotification(
|
||||
{ ...makeNotification(mergeCallId, 'HIGH', '请 王小明 同学找 张老师'), kind: 'call' },
|
||||
{ inClass: false },
|
||||
);
|
||||
await sleep(250);
|
||||
island.pushNotification(makeNotification(mergeCallId, 'HIGH', '请 王小明 同学找 张老师'), {
|
||||
inClass: false,
|
||||
});
|
||||
await sleep(450);
|
||||
const mergedCall = island.getState();
|
||||
record(
|
||||
'双广播(叫人 + 通知)不降级:同 id 合并后仍按"叫人"处理',
|
||||
mergedCall.active?.id === mergeCallId && mergedCall.active?.kind === 'call' && mergedCall.mode === 'pill',
|
||||
`mode=${mergedCall.mode} kind=${mergedCall.active?.kind ?? '-'} priority=${mergedCall.active?.priority ?? '-'}`,
|
||||
);
|
||||
const mergeUrgentId = 'smoke-call-merge-urgent';
|
||||
island.pushNotification(makeNotification(mergeUrgentId, 'NORMAL', '请 王小明 同学找 张老师'), {
|
||||
inClass: true,
|
||||
});
|
||||
await sleep(250);
|
||||
island.pushNotification(
|
||||
{ ...makeNotification(mergeUrgentId, 'URGENT', '请 王小明 同学找 张老师'), kind: 'call' },
|
||||
{ inClass: true },
|
||||
);
|
||||
await sleep(450);
|
||||
const mergedUrgent = island.getState();
|
||||
record(
|
||||
'紧急叫人双广播:合并后仍 URGENT 立即展开(课中不被降级为暂存)',
|
||||
mergedUrgent.active?.id === mergeUrgentId &&
|
||||
mergedUrgent.mode === 'expanded' &&
|
||||
mergedUrgent.active?.priority === 'URGENT',
|
||||
`mode=${mergedUrgent.mode} priority=${mergedUrgent.active?.priority ?? '-'} kind=${mergedUrgent.active?.kind ?? '-'}`,
|
||||
);
|
||||
island.setClassState({ inClass: false, currentPeriodEnd: null });
|
||||
island.handleAction({ action: 'dismiss' });
|
||||
await sleep(300);
|
||||
|
||||
// 4.13) 渲染进程崩溃自愈:岛窗口绝不能变成"屏幕上一帧点不动的幽灵胶囊"。
|
||||
// 真实现象(本机实测):崩掉岛渲染进程后,Windows 保留最后一帧 —— 胶囊看着还在,
|
||||
// 窗口也还在,但点击全部丢失(主进程日志里再无"收到用户动作")。
|
||||
// 这里断言主进程会隐藏死窗口并重建,重建后状态回来了、也能重新命中。
|
||||
island.pushNotification(makeNotification('smoke-renderer-recover', 'NORMAL', '渲染进程自愈'), {
|
||||
inClass: false,
|
||||
});
|
||||
await sleep(500);
|
||||
const windowBeforeCrash = island.getWindow();
|
||||
islandWindow?.webContents.forcefullyCrashRenderer();
|
||||
await sleep(3000);
|
||||
const windowAfterCrash = island.getWindow();
|
||||
const recovered = island.isReady() && Boolean(windowAfterCrash) && windowAfterCrash !== windowBeforeCrash;
|
||||
island.setHitTestCursor(islandCardScreenPoint(0.5, 0.5));
|
||||
await sleep(250);
|
||||
const recoveredInteractive = island.getInteractive();
|
||||
const recoveredState = island.getState();
|
||||
record(
|
||||
'渲染进程崩溃后自愈(隐藏死窗口 + 重建 + 恢复命中,不再留"点不动的幽灵胶囊")',
|
||||
recovered && recoveredInteractive && recoveredState.mode === 'pill',
|
||||
`窗口已重建=${windowAfterCrash !== windowBeforeCrash} ready=${island.isReady()} ` +
|
||||
`mode=${recoveredState.mode} 命中=${recoveredInteractive}`,
|
||||
);
|
||||
island.setHitTestCursor(null);
|
||||
island.handleAction({ action: 'dismiss' });
|
||||
await sleep(300);
|
||||
|
||||
// 收尾:隐藏灵动岛,避免影响后续用例
|
||||
island.handleAction({ action: 'dismiss' });
|
||||
await sleep(300);
|
||||
|
||||
Reference in new issue
Block a user