docs(desktop,island): 修正被写成既成事实的"收回握手"机制描述

AGENTS.md 第 34 条把一个从未落地的设计当成了已实现的规则,还配上了"踩过的坑"与
"两条配套铁律",读起来像历史教训。实际核对:`island:closing`、`island:closing-done`、
`sendClosingDone`、`noteClosingDone`、`closingTimer`、`finishClosing` 六个符号在全部
源码中零命中,在 git 全部历史里也从未出现过。

真实实现是主进程单向的:handleAction() → dismissActive() / clearPendingForClose()
→ hide() → fadeOut(),状态同一帧写成 hidden,不等渲染进程任何回执。渲染侧只有本地
弹簧形变(spring.ts),是纯视觉动画,不参与淡出时机。

另,早先那层自研的"卡片缩回胶囊再淡出"快照动画(closing / closingSnapshot)确实存在过,
因在触摸屏上会露出旧帧/重影,已按用户要求整体删除 —— IslandApp.vue 顶部注释有记载。

改动:重写 AGENTS.md 第 34 条为真实机制,并注明该自研动画已移除、别再照着不存在的
握手机制改代码;README 三处同样表述一并修正;smoke.ts 里那句"渲染进程收敛上报"的
过时注释改为描述真实的淡出轮询。
This commit is contained in:
LoveChengke committed 2026-10-02 00:32:48 +08:00
1 parent 20fcca0388
commit 09433a572c
3 files changed
+28 -22

No files matched your search

+20 -15
View File
@@ -399,22 +399,27 @@ homeworks / notifications / calls / imports / integrations / grades / students /
写这类用例时注意:**断言"单条卡片"的用例(紧急卡/叫人卡/截图比例)必须先 `drainIsland()` 排空队列**,
否则量到的是列表高度。
34. **"知道了 / 标为已读"之后要有收回动画,但状态必须同步落地。**
状态机与动画分开:主进程先发 `island:closing`(渲染进程据此把当前卡片按"胶囊形态 + 这批消息"收回),
**同时**把状态写成 `hidden` / 清空(调用方与冒烟按同步语义断言),窗口随后才淡出隐藏。
`closing` 期间的守卫别删:`syncWindow` 要保持窗口可见、`relayout` 不许把窗口收小
(收小会把正在缩回的卡片裁掉;真正收小在窗口隐藏之后,见 `syncWindow` 里"不可见时对齐包围盒")。
多条通知时这两个按钮都是**整批**操作:`dismiss-all` / `mark-all-read`(作业卡的合成 id
`homework-<id>` 要在主进程按 kind 过滤掉,否则通知中心会去查一条不存在的通知)。
回归用例:`verify:desktop` 的「多条通知点「知道了」:整批关闭 + 收回动画」。
34. **"知道了 / 标为已读"之后是「状态同步落地 + 窗口淡出」,没有跨进程的收回握手。**
收起一律是**主进程单向**的:`handleAction()` 收到 `dismiss` / `dismiss-all` / `mark-read` /
`mark-all-read` → `dismissActive()`(还有下一条则退胶囊)或 `clearPendingForClose()`
→ `hide()` → `fadeOut()`(每帧 `setOpacity(-0.2)`,到 0.02 后 `win.hide()`)。
状态在**同一帧就写成** `hidden` / 清空,**不等渲染进程任何回执** —— 调用方与冒烟按"同步语义"断言
(`island.ts` 的 `clearPendingForClose()` 注释:「状态同步落地(渲染进程随之收起卡片,窗口由 `hide()` 淡出)」)。
渲染进程那侧只有本地弹簧形变(`spring.ts` 的 `targetSize/progress`),是纯视觉动画,不参与淡出时机。
**什么时候淡出不能靠"猜时长"**:渲染进程形变收敛后会 `sendClosingDone()` 上报
(`island:closing-done`),主进程 `noteClosingDone()` 才淡出;`closingTimer` 只是**兜底上限**
(`max(duration*2, 1200+duration)`,覆盖 rAF 被节流到连 MORPH_FALLBACK 的 1.2s snap 都没跑完的情形)。
早期实现按固定 360ms 猜,rAF 一被节流就"卡片缩到一半窗口没了"(用户反馈的"收回动画截断灵动岛")。
**两个配套铁律**:①`finishClosing()` 里"定时器句柄置空"必须与 `clearTimeout` 成对
(只置空=留下幽灵定时器,会在动画正常结束之后又静默隐藏一次,把随后弹出的紧急通知吃掉 —— 踩过);
②渲染侧 `closing` 快照的清理定时器要给足余量(现在 `durationMs + 1000`),它只负责丢快照。
**曾经有过一层自研的"收回快照"动画,已按用户要求整体删除。** 它叫 `closing` / `closingSnapshot`:
点"知道了"后把卡片按"胶囊摘要"收回再淡出,是自研的形变状态机,**实测在触摸屏上会露出旧帧/重影**
(触摸模式窗口贴合岛体、卡片形变时窗口跟着动),用户明确要求"动画就用仓库里那套",于是整段删掉 ——
现状见 `IslandApp.vue` 顶部注释。**别照着一份不存在的握手机制去改代码。**
(`smoke.ts` 里"收回动画是渲染进程收敛上报 → 主进程淡出"只是一句过时注释,代码里没有这条链路。)
多条通知时这两个按钮都是**整批**操作:`dismiss-all` / `mark-all-read`(作业卡的合成 id
`homework-<id>` 要在主进程按 kind 过滤掉,否则通知中心会去查一条不存在的通知 ——
见 `handleAction()` 里 `mark-all-read` 分支的 `.filter((item) => item.kind !== 'homework')`)。
回归用例:`verify:desktop` 的「多条通知点「知道了」:整批关闭(状态同步清空,窗口淡出隐藏)」。
排查"收回时闪一下"这类反馈时注意:**先看是不是自动化验证实例的叠影**(见第 41、42 条),
别往这套已经不存在的握手机制上找原因。
35. **本机录入的作业不再上灵动岛。** 服务端是**先广播 `homework:new`、后回响应**的,
等响应拿到 id 再登记就晚了(事件可能已经到了、岛已经弹出来了)——所以
+6 -5
View File
@@ -499,7 +499,7 @@ pnpm verify:packaged # 对**打包后/已安装**的客户端 EXE 跑同一
| 点击胶囊(只有一条) | 展开为**详情卡**(形变 + 淡入,无回弹):标题、内容、时间、`打开应用 / 标为已读 / 知道了` |
| **点击胶囊(多条)** | 展开为**竖排列表**:按**重要程度**排序(紧急 > 重要 > 普通 > 低,同级按时间新的在前),默认**只显示前三条**,下面一行 `展开更多(还有 N 条)`;列表始终只有**一排按钮** |
| **点"展开更多"** | 把放得下的通知都铺出来;铺到屏幕工作区下沿(任务栏)仍放不下时,不再硬撑,改成 `更多请前往应用内操作`(卡片高度始终 ≤ 可用高度) |
| **多条时的"知道了 / 标为已读"** | 都是**整批**操作:`知道了` 整批关闭(通知中心仍为未读)、`标为已读` 把这一批全部标为已读并关闭;两者都有**收回动画**(卡片先缩回胶囊、再淡出窗口) |
| **多条时的"知道了 / 标为已读"** | 都是**整批**操作:`知道了` 整批关闭(通知中心仍为未读)、`标为已读` 把这一批全部标为已读并关闭;两者都是状态同步清空 + 窗口淡出隐藏 |
| **上课时段的"普通叫人"** | **不自动展开**(不打断课堂),但保留 `叫人` 胶囊并**允许学生主动点开**;下课自动展开(`isOpenable`:紧急消息与任何叫人都可点开) |
| **收起后胶囊常驻** | 自动收起 / 手动收起只把卡片回缩为胶囊,**有未处理通知时胶囊不会消失**;彻底消失只在"知道了 / 标为已读"、队列清空或进入上课时段 |
| **收起态点击命中** | 收起瞬间窗口会保持可交互,且渲染进程在每次状态变化后都按**缓存的指针位置**重算命中 |
@@ -527,9 +527,10 @@ pnpm verify:packaged # 对**打包后/已安装**的客户端 EXE 跑同一
`islandListLayout()` 统一算出(度量表 `ISLAND_LIST_METRICS`)——**主进程与渲染进程各算一次同一个数**:
主进程据此把窗口包围盒长高(列表比普通展开卡高,窗口不够高会把底部按钮裁掉),渲染进程据此排版。
改 `IslandApp.vue` 里列表相关 CSS 的尺寸必须同步改那份度量,否则会出现"最后一行被切掉"。
- **点"知道了 / 标为已读"后的收回动画**:状态是**同步**落地的(调用方与冒烟立刻看到清空),
窗口则等渲染进程把卡片缩回胶囊再淡出(主进程先发 `island:closing`,渲染进程按"胶囊形态 + 这批消息"
的摘要收起)。收回期间窗口保持可见、不接收鼠标,动画结束才真正隐藏。
- **点"知道了 / 标为已读"后的收起**:状态是**同步**落地的(调用方与冒烟立刻看到清空),
窗口随即淡出隐藏(主进程 `handleAction()` → `dismissActive()` / `clearPendingForClose()` → `hide()`
→ `fadeOut()`),**没有跨进程的收回握手**。早先有一层自研的"卡片缩回胶囊再淡出"快照动画,
因在触摸屏上会露出旧帧/重影,已按用户要求整体删除。
- **本机录入的作业不上岛**:服务端是"先广播 `homework:new`、后回响应"的,因此 `renderer/island/bridge.ts`
在**发请求之前**就按内容登记指纹(班级 + 日期 + 标题 + 正文),回执拿到 id 后再补记一次;
投递前用 id / 指纹双重判定,命中就不上岛(冒烟有专项断言)。
@@ -1236,7 +1237,7 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
| 灵动岛:收回无"方框"闪烁 / 点击屏幕任意处收回 | ✅ | 逐帧采样卡片尺寸恒定(窗口固定包围盒)+ 失焦自动收回 |
| 灵动岛"标为已读"同步通知中心 | ✅ | 真实链路:点击后 `read=false → true`、未读数减少 |
| **多条通知展开为竖排列表** | ✅ | `verify:desktop`:按重要程度排序、默认 3 行 + `展开更多(还有 N 条)`、卡片高度与 `islandListLayout()` 一致;点"展开更多"铺满;放不下时提示 `更多请前往应用内操作` 且卡片高度 ≤ 可用高度 |
| **多条通知的整批操作 + 收回动画** | ✅ | `verify:desktop`:`知道了` 整批关闭且收回期间窗口仍在屏幕上、随后隐藏;`标为已读` 经真实链路把**两条**通知都标为已读 |
| **多条通知的整批操作** | ✅ | `verify:desktop`:`知道了` 整批关闭且状态同步清空、窗口随后淡出隐藏;`标为已读` 经真实链路把**两条**通知都标为已读 |
| **本机录入的作业不再上岛** | ✅ | `verify:desktop`:按 id 与内容指纹双重判定,投递后灵动岛状态不变;别的作业照常上岛 |
| 作业发布也上岛("新作业"胶囊) | ✅ | `kind=homework` + 展开显示作业要求;**截止时间功能已下线**(接口不再返回 `dueAt`,e2e 有断言) |
| **灵动岛收起后始终可再次打开** | ✅ | `verify:desktop`:收起后胶囊常驻、「收起态点击可再次展开」、「空闲细缝态保持可交互」 |
+2 -2
View File
@@ -1338,8 +1338,8 @@ async function runIslandChecks(
island.hide();
await sleep(400);
// 排空这一节留下的队列:下面断言的是"单条通知的胶囊 / 详情卡"(多条会走列表形态),
// 顺带等"收回动画"播完(窗口真正隐藏,hiddenBeforePush 才有意义)——
// 收回动画是"渲染进程收敛上报 → 主进程淡出",rAF 节流时比固定时长更久,轮询等它真的隐藏。
// 顺带等窗口真的隐藏(hiddenBeforePush 才有意义)。
// 淡出是主进程逐帧 setOpacity 的动画,机器忙时会比固定时长更久,因此轮询等它隐藏。
await drainIsland();
let hiddenBeforePush = !(islandWindow?.isVisible() ?? true);
for (let attempt = 0; attempt < 25 && !hiddenBeforePush; attempt += 1) {