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
@@ -213,6 +213,10 @@ const progress = ref(0);
|
||||
|
||||
let rafHandle: number | null = null;
|
||||
let lastFrameAt = 0;
|
||||
/** 形变兜底定时器:rAF 被系统限流时用它 snap 到目标几何(见 startFrameLoop) */
|
||||
let morphFallbackTimer: number | null = null;
|
||||
/** 兜底等待时长:正常形变约 300~500ms,超出这个时间说明帧被限流了 */
|
||||
const MORPH_FALLBACK_MS = 1200;
|
||||
|
||||
function syncTargets(snap = false): void {
|
||||
const size = targetSize();
|
||||
@@ -237,6 +241,24 @@ function syncTargets(snap = false): void {
|
||||
function startFrameLoop(): void {
|
||||
if (rafHandle !== null) return;
|
||||
lastFrameAt = performance.now();
|
||||
// 形变兜底:某些情况下(Windows 把置顶透明小窗判定为"被遮挡/后台"、GPU 进程刚重启等)
|
||||
// requestAnimationFrame 会被 Chromium 压到 1 帧/秒 —— 实测打包版细缝态出现过 1.1fps。
|
||||
// 那种情况下"点击后形变"看起来就是**没反应**(用户反馈的"点不动")。因此这里挂一个定时器,
|
||||
// 到点若还没收敛就直接 snap 到目标几何:宁可少一段动画,也不能让点击看上去无效。
|
||||
if (morphFallbackTimer !== null) window.clearTimeout(morphFallbackTimer);
|
||||
morphFallbackTimer = window.setTimeout(() => {
|
||||
morphFallbackTimer = null;
|
||||
if (rafHandle === null) return; // 已经收敛
|
||||
if (rafHandle !== null) window.cancelAnimationFrame(rafHandle);
|
||||
rafHandle = null;
|
||||
const size = targetSize();
|
||||
springs.width.snap(size.width);
|
||||
springs.height.snap(size.height);
|
||||
springs.radius.snap(targetRadius(size));
|
||||
morph.value = { width: size.width, height: size.height, radius: springs.radius.value };
|
||||
progress.value = mode.value === 'expanded' ? 1 : 0;
|
||||
refreshInteractive();
|
||||
}, MORPH_FALLBACK_MS);
|
||||
const frame = (now: number): void => {
|
||||
const delta = now - lastFrameAt;
|
||||
lastFrameAt = now;
|
||||
@@ -270,6 +292,10 @@ function startFrameLoop(): void {
|
||||
springs.radius.snap(targetRadius(size));
|
||||
morph.value = { width: size.width, height: size.height, radius: springs.radius.value };
|
||||
progress.value = mode.value === 'expanded' ? 1 : 0;
|
||||
if (morphFallbackTimer !== null) {
|
||||
window.clearTimeout(morphFallbackTimer);
|
||||
morphFallbackTimer = null;
|
||||
}
|
||||
refreshInteractive();
|
||||
rafHandle = null;
|
||||
};
|
||||
@@ -457,6 +483,10 @@ function applyAppearance(next: IslandAppearance | null | undefined): void {
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('mousemove', (event) => updateInteractive(event.clientX, event.clientY));
|
||||
// 心跳:主进程据此判断本渲染进程是否还活着。一旦卡死/崩溃,主进程会隐藏这扇"幽灵窗口"
|
||||
// 并重建(否则 Windows 会把最后一帧留在屏幕上:岛看着还在,怎么点都没反应)。
|
||||
bridge?.alive?.();
|
||||
window.setInterval(() => bridge?.alive?.(), 5000);
|
||||
bridge?.onState((next) => {
|
||||
state.value = next;
|
||||
syncTargets();
|
||||
|
||||
Reference in new issue
Block a user