dsh-later 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/README.en.md +55 -2
- package/README.md +53 -2
- package/icon.svg +10 -0
- package/lib/bar-geometry.js +139 -0
- package/lib/client.js +1349 -423
- package/lib/command-outcome.js +15 -0
- package/lib/commands.js +56 -8
- package/lib/delivery.js +66 -0
- package/lib/domain.js +6 -0
- package/lib/index.js +152 -29
- package/lib/ownership-store.js +113 -15
- package/lib/presence.js +122 -4
- package/lib/projection.js +149 -20
- package/lib/runtime.js +160 -24
- package/lib/schedule-compat.js +179 -0
- package/lib/schedule-summary-store.js +160 -0
- package/lib/schedule-summary.js +113 -0
- package/lib/types/bar-geometry.d.ts +111 -0
- package/lib/types/command-outcome.d.ts +19 -0
- package/lib/types/delivery.d.ts +51 -0
- package/lib/types/domain.d.ts +32 -0
- package/lib/types/index.d.ts +28 -8
- package/lib/types/ownership-store.d.ts +42 -2
- package/lib/types/presence.d.ts +75 -4
- package/lib/types/projection.d.ts +68 -6
- package/lib/types/runtime.d.ts +58 -1
- package/lib/types/schedule-compat.d.ts +111 -0
- package/lib/types/schedule-summary-store.d.ts +75 -0
- package/lib/types/schedule-summary.d.ts +92 -0
- package/lib/types/upstream-compat.d.ts +123 -0
- package/lib/types/user-tools.d.ts +48 -7
- package/lib/upstream-compat.js +143 -0
- package/lib/user-tools.js +268 -44
- package/package.json +25 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,253 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录面向使用者的重要变更。细粒度提交历史见 git log。
|
|
4
4
|
|
|
5
|
+
## 未发布
|
|
6
|
+
|
|
7
|
+
## 1.2.0 — 2026-10-09
|
|
8
|
+
|
|
9
|
+
- **feat** 暴露跨插件「定时任务摘要」公共 API(issue #17)。
|
|
10
|
+
- 新增 `src/schedule-summary.ts`(纯函数,宿主/浏览器/`node --test` 共用):
|
|
11
|
+
`buildScheduleSummary` 把 `sessions.list` 折叠成
|
|
12
|
+
`Map<SessionId, { count, pausedCount, nextAt, state }>`;`summarizeScheduleGroup`
|
|
13
|
+
按分组聚合(会话数/任务数/最早触发/最高优先级,`overdue > urgent > scheduled`)。
|
|
14
|
+
- 新增 `src/schedule-summary-store.ts`:可订阅 store(`getSnapshot` / `subscribe` /
|
|
15
|
+
`reload` / `dispose`)+ 全局登记表,**唯一数据权威**——内部负责 `sessions.list`
|
|
16
|
+
订阅、30s 状态 tick 与 60s `session.list` 读 RPC(仅页面可见时)。
|
|
17
|
+
- 新增 `src/client/useScheduleSummary.ts`:React 订阅 hook(`useSyncExternalStore`,
|
|
18
|
+
store 迟到时自动重渲染)。
|
|
19
|
+
- `lib/client.js` 导出 `getScheduleSummaryStore` / `subscribeScheduleSummaryStore` /
|
|
20
|
+
`useScheduleSummary` / `buildScheduleSummary` / `schedulesOfProjection` /
|
|
21
|
+
`summarizeScheduleGroup` / `createScheduleSummaryStore` 及类型;消费方(如
|
|
22
|
+
dsh-group-status)声明 `dsh.client.external: ["dsh-later"]` 后
|
|
23
|
+
`require('dsh-later/client')` 即可使用。契约见
|
|
24
|
+
[API 参考 §7](./docs/technical/03-api-reference.md)。
|
|
25
|
+
- **refactor** 侧栏 badge 不再自己读 `sessions.list` / 周期 `refresh()`,改为消费同一个
|
|
26
|
+
store:外部插件读到的摘要与本插件行内 badge 必然同源,且 `session.list` 读 RPC
|
|
27
|
+
不再被拉两次。
|
|
28
|
+
- 新增 15 个单测:纯逻辑(数量/状态分档/暂停语义/分组聚合/判等)、store
|
|
29
|
+
(列表变化、状态 tick、refresh 闸门、dispose、登记表)与构建产物契约
|
|
30
|
+
(`lib/client.js` 导出面 + apply 登记/注销 store)。
|
|
31
|
+
|
|
32
|
+
- **feat** 侧栏 badge 进度环(方案 A:track + prog 双环)。
|
|
33
|
+
- 新版时钟 glyph 从单环改为双环:`ss-clock-track` 恒定底环(总窗口参照),
|
|
34
|
+
`ss-clock-prog` 进度弧从 12 点顺时针填充(`stroke-dashoffset` 驱动)。
|
|
35
|
+
- 新增 `progressFrom` 字段(`PresenceSummary`):窗口起点时刻(epoch ms),
|
|
36
|
+
只跟随 `nextAt` 那条任务。
|
|
37
|
+
- 新增 `windowStartOf` 推导逻辑,按优先级取窗口跨度:
|
|
38
|
+
1. `window_seconds`(宿主/投影显式给)→ 最权威
|
|
39
|
+
2. `after_seconds` / `every_seconds`(按 `kind` 推)
|
|
40
|
+
3. `created_at`(`at` 类型回退)
|
|
41
|
+
推不出则 `progressFrom` 缺省,视觉退回恒定整环(旧形状即退化态)。
|
|
42
|
+
- `progressOf(entry, now)` 返回 0–1 比例;超时停满环(不绕出去)。
|
|
43
|
+
- urgent 只脉冲底环(进度弧跟着闪会让比例不可读)。
|
|
44
|
+
- 减少动效模式:进度弧位移过渡一并停。
|
|
45
|
+
- 新增 11 个单测:after/every/at 窗口推导、window_seconds 优先、退化/非法值、
|
|
46
|
+
多任务窗口跟随 nextAt、暂停项排除、camelCase 兼容、无窗口时形状不变。
|
|
47
|
+
- `sameScheduleSummary` 补 `progressFrom` 比对(暂停→恢复窗口换了但 nextAt 未变时
|
|
48
|
+
仍需通知订阅者刷新进度环)。
|
|
49
|
+
|
|
50
|
+
## 1.1.0 — 2026-10-09
|
|
51
|
+
|
|
52
|
+
- **feat** 设置页 UI 对齐官方 `SettingsForm`:从折叠卡片改为裸表单,字段直接平铺。
|
|
53
|
+
- 移除 `ss-card` 容器、折叠头部、chevron、未保存徽章(`ss-card-pending`)。
|
|
54
|
+
- 布尔字段改用官方 toggle switch(`role="switch"`),不再用 select + 开/关文案。
|
|
55
|
+
- CSS 类名对齐官方(`ss-settings-form`、`ss-toggle-row`、`ss-toggle-switch`)。
|
|
56
|
+
- 清理死代码:移除 `onKey`/`offKey` 字段声明及 `optionOn`/`optionOff`/`optionEnabled`/`optionDisabled` 字典键。
|
|
57
|
+
|
|
58
|
+
- **feat** 新增 `schedule-compat` 兼容层:统一处理不同 DSH 内核代次的 schedule 编解码差异。
|
|
59
|
+
- 新增 `src/schedule-compat.ts`(226 行)及其编译产物 `lib/schedule-compat.js`。
|
|
60
|
+
- 上游跨代兼容逻辑收敛到单点维护,新 rc 落地只需改矩阵 + 重跑 sync 脚本。
|
|
61
|
+
|
|
62
|
+
- **feat** 插件列表显示专属图标:新增包根 `icon.svg`,并在 `package.json` 声明
|
|
63
|
+
顶层 `icon` 字段。
|
|
64
|
+
- 图形沿用侧栏「定时状态」badge 的几何(圆环 + 上/右指针),配色改为
|
|
65
|
+
官方插件图标的蓝色渐变区间(`#7AC2FF → #4A65E8`),使其与官方图标同属一个家族。
|
|
66
|
+
- 配色决策依据:实测 4 个带图标的官方包,**全部硬编码十六进制,无一使用
|
|
67
|
+
CSS 变量或 `currentColor`**(插件图标经 `dsh-app-boot` 的 `iconOf()` 读成
|
|
68
|
+
base64 data URL 以 `<img>` 渲染,因此**不执行 CSS 动画、不继承页面颜色**)。
|
|
69
|
+
统一感来自「取色区间 + 画法」而非令牌系统,故采用同区间色号。
|
|
70
|
+
- 侧栏 badge 保留品牌紫 `#8b5cf6`:那是插件自身语境下的**状态色**
|
|
71
|
+
(紫 / warn / error 三态区分),与插件列表的外部语境不冲突。
|
|
72
|
+
- `icon.svg` 已加入 `files`,避免 `npm publish` 漏发导致图标丢失。
|
|
73
|
+
|
|
74
|
+
- **fix** 支持 DSH `0.2.0`(此前在 0.2.0 上**完全失效**)。
|
|
75
|
+
- 现象:升级到 DSH `0.2.0-rc.2` 后插件整体不加载 —— 无按钮、无命令、无工具。
|
|
76
|
+
宿主日志:`skipping profile bundle "dsh-later": Plugin dsh-later@1.0.0 is
|
|
77
|
+
incompatible with dsh 0.2.0-rc.2 …`。
|
|
78
|
+
- 根因:**不是代码失效,是 peer 声明缺口**。内核自 0.2.0 起把 peer 兼容校验
|
|
79
|
+
**从警告升级为硬门槛**(`dsh-app-boot` 的 `evaluatePluginCompatibility`):
|
|
80
|
+
peer 范围不满足即**整插件跳过**。而 peer 枚举止于 `^0.1.7-rc.2`,
|
|
81
|
+
8 个 kernel-scoped 包对 `0.2.0-rc.2` 全部不满足(预发布版必须显式枚举)。
|
|
82
|
+
- 核对结论:**API 面全部保持,无需改动任何业务代码**。Session 形状未变;
|
|
83
|
+
`dsh-schedule` 导出面完整(`renderRecurringReminderBatchFraming` 仍在,
|
|
84
|
+
0.1.7 的桥接照旧命中);`dsh-llm` 的 `createUserMessage` /
|
|
85
|
+
`boundContextSummary` 在,`schedule` message source 仍有效;
|
|
86
|
+
`dsh-agent` 的 `followup` / `steer` 语义未变;`defineTool` 在。
|
|
87
|
+
- 实证:devDependencies 固定到 `0.2.0-rc.2` 后,host + client `tsc --noEmit`
|
|
88
|
+
**双端 0 错误**,`npm run build` 走 tsc 原生路径(无 esbuild 降级),测试全绿。
|
|
89
|
+
- 处置:`KERNEL_GENERATIONS` 追加 `0.2.0-rc.2` → 重跑 `sync-peer-matrix.mjs`
|
|
90
|
+
重写 8 个 peer 范围 → dev 推进到 `0.2.0-rc.2`。历史代次仍全部支持,
|
|
91
|
+
未核对的 `0.3.0-rc.1` 仍被正确拒绝。
|
|
92
|
+
- **fix** 多条合并提醒的 framing 措辞对齐上游(`0.2.0` 的第二处差异)。
|
|
93
|
+
- `dsh-schedule` 在 0.2.0 把 one-shot 与 recurring 的防护句统一收敛为
|
|
94
|
+
`This is a scheduled message from the user`(旧句为
|
|
95
|
+
`Present reminder_prompt_json … not new user instructions.`)。
|
|
96
|
+
- 单条路径直接调上游函数,自动跟随;但**多条到期一次性合并**(P1-5)是上游
|
|
97
|
+
不提供的形态,由插件自渲染,其来源行是上游文案的**本地副本**,需手工对齐,
|
|
98
|
+
否则同一会话里单条与合并提醒的措辞会不一致。
|
|
99
|
+
- 新增护栏:从上游 `renderReminderFraming` **实时取**来源行比对本地副本,
|
|
100
|
+
不硬编码任何一版文案(故支持任意多代)。反向验证:改回旧长句即失败。
|
|
101
|
+
- 注:该问题被 peer 缺口**掩盖**——插件不加载时 framing 根本不会执行,
|
|
102
|
+
修完 peer 后测试才暴露出来。
|
|
103
|
+
|
|
104
|
+
- **fix** 本地开发依赖代次与运行时代次对齐(`0.1.2-rc.1` → `0.1.5-rc.1`)。
|
|
105
|
+
- 起因:`devDependencies` 原先写的是**完整 peer 范围串**,包管理器因此解析到
|
|
106
|
+
「满足范围的最老可用版本」= `0.1.2-rc.1`,而真实运行环境是 `0.1.5-rc.3` ——
|
|
107
|
+
**测试与类型检查落后运行环境整整一代**,API 漂移在本地无从暴露。
|
|
108
|
+
上一个 peer 缺口正是这样漏掉的(本地怎么测都发现不了)。
|
|
109
|
+
- 处置:`devDependencies` 改为**固定到最新已验证代次**(全部 kernel-scoped 包同代次);
|
|
110
|
+
`peerDependencies` 仍为完整范围串,两者意图不同、不再混同。
|
|
111
|
+
- `scripts/sync-peer-matrix.mjs` 同步改造:peer 写范围、dev 写固定代次;且只在 dev
|
|
112
|
+
已是范围串或缺失时才改写,避免把有意固定的版本冲掉(保持幂等)。
|
|
113
|
+
- 结果:`npm run typecheck` 通过且无新增错误;`npm test` 183 通过 / 0 失败。
|
|
114
|
+
- 新增守卫:dev 必须是具体代次而非范围串、必须落在已验证列表内、全部包同代次、
|
|
115
|
+
且等于最新已验证代次;另有显式登记当前 dev 代次的断言,使代次变更在 diff 中可见。
|
|
116
|
+
- **fix** 补 `0.1.5` 内核代次的 peer 声明:此前只声明 `^0.1.1-rc.2 || ^0.1.2-rc.1`,
|
|
117
|
+
而实际运行的宿主早已是 `0.1.5-rc.3` —— 在真实 `npm install` 下**不满足声明范围**
|
|
118
|
+
(`semver.satisfies` 默认预发布语义为 false)。本地经 profile symlink 加载,
|
|
119
|
+
peer 不参与解析,所以一直没暴露。
|
|
120
|
+
- 实测确认 `0.1.5-rc.1/rc.2/rc.3` 可支持:隔离安装后 15/15 host 模块导入通过,
|
|
121
|
+
且 `apply()` 以最小假 ctx 实例化成功;与原有 `0.1.2-rc.1` 的注册顺序逐项相同。
|
|
122
|
+
- **未跟进 `0.1.7`**:该代有破坏性变更,实测 15 个模块中 4 个因缺符号失败 ——
|
|
123
|
+
`allocateScheduleId` 导出被删(插件 5 处调用)、`renderEveryReminderBatchFraming`
|
|
124
|
+
改名为 `renderRecurringReminderBatchFraming`、`createAfterScheduleRecord` 等
|
|
125
|
+
新增必填 `title` 参数。适配是独立工作项,不能靠加一行 peer 蒙混过去。
|
|
126
|
+
- 代次守卫测试改为数据驱动(原先硬编码两代),并加强:`peerRange` 下界必须恰为该
|
|
127
|
+
代次本身(不借用更早下界),代次表严格递增。
|
|
128
|
+
|
|
129
|
+
- **fix** 「插话发送」会**静默丢失**:提醒从列表消失,消息却没送达模型。
|
|
130
|
+
- 根因:`agent.steer()` 入 `next-step`,语义是「下一个 step 边界消费」。
|
|
131
|
+
若插话恰好撞上 `turn/start`,核心 `inbox.claim('next-step', …)` 会把**整批**
|
|
132
|
+
取走交给一个**请求上下文已定**的 step —— 消息落进 surface,却不进实际请求。
|
|
133
|
+
- 实测时序(会话日志,毫秒级):
|
|
134
|
+
```
|
|
135
|
+
t+0ms agent/inbox/spliced → 插入 next-step
|
|
136
|
+
t+1ms turn/start → turn 17 开始(同一毫秒)
|
|
137
|
+
t+1ms agent/inbox/spliced → removedCount:1, inserted:[] ← 被 claim 吞掉
|
|
138
|
+
t+17ms request/header → 请求已按旧上下文构造
|
|
139
|
+
```
|
|
140
|
+
该轮模型回复与提醒内容无关,确认它从未读到这条消息。
|
|
141
|
+
- **并有第二个独立缺陷**:旧实现在 `steer()` 之后**无条件**写 `dispatch`,
|
|
142
|
+
把提醒移出 active。投递失败 = 提醒永久消失且**不可重试** ——
|
|
143
|
+
虚假的成功信号掩盖了真实的投递失败。
|
|
144
|
+
- 修复一(默认投递方式 `followup`):入 `next-turn`,与 `next-step` 是两条独立
|
|
145
|
+
队列,`claim('next-step', …)` 不触碰它,**竞态在结构上不存在**;语义等价于
|
|
146
|
+
「正常发送」(独立成轮)。可在设置里改回 `steer`。
|
|
147
|
+
- 修复二(投递确认,默认开):投递后**读会话日志**核对 message id 是否真的落盘。
|
|
148
|
+
未确认且「未确认时 = 保留条目」时**不写 dispatch**,提醒留在列表中可重试,
|
|
149
|
+
并在行内明确提示 —— 不再静默。
|
|
150
|
+
- 未确认处理可选「丢弃」(恢复旧语义);选择时 UI 明示「静默丢失将重新变得可能」。
|
|
151
|
+
- 确认机制**只依赖已持久化的日志格式**,不监听 `inbox/claimed` / `request/header`
|
|
152
|
+
等核心运行时事件,避免与核心内部时序隐性耦合。
|
|
153
|
+
- 设置 UI:新增「插话投递」三项目;选 `steer` 时常驻显示风险提示,且文案随
|
|
154
|
+
「投递确认」开关切换口径(关确认时明说不会保留条目)。
|
|
155
|
+
- **fix** 「插话发送」对普通(非暂停)提醒报 `schedule_not_found`。
|
|
156
|
+
- 根因:GUI 传的是**投影里的 id**,而它「稳定 uid 优先」
|
|
157
|
+
(`wireItemOf`:`id: uid ?? record.id`);日志记录却用 `schedule-N`。
|
|
158
|
+
`steerById` 直接 `ScheduleId(id)` 后按 `r.id === scheduleId` 比对,
|
|
159
|
+
**没有做 uid → 日志 id 的换算**,因此所有由用户工具创建的提醒插话都失败。
|
|
160
|
+
- 实测:uid `f0fc0c90…` vs 日志 id `schedule-1`,必然不相等。
|
|
161
|
+
- 删除 / 暂停路径早已用 `resolveActiveScheduleId` 做了这层换算,插话是漏网的。
|
|
162
|
+
- 修复:插话先按 uid 查当前活动的日志 id(`findScheduleIdByUid`),
|
|
163
|
+
找不到再当作日志 id 原样使用(兼容无 uid 的旧数据与直接传 `schedule-N`)。
|
|
164
|
+
dispatch 事件仍写**日志 id**,不是 uid。
|
|
165
|
+
- **fix** 进度条「速度不均衡 / 每秒一跳」:相位参数被逐秒重算导致双倍计数。
|
|
166
|
+
- 根因:CSS 动画挂上后会**自行推进**;而 `useNow(1000)` 每秒触发重渲染,
|
|
167
|
+
每次都把 `delay = -(now - start)` 重算一遍,浏览器便把播放头又前移 1 秒
|
|
168
|
+
—— 这一秒动画本已走过,于是**每秒多跳一次**。
|
|
169
|
+
- 实测证据:`--ss-bar-delay` 每约 1s 精确变化 **-1000ms**;宽度增量出现
|
|
170
|
+
`0.0153 → 0.3485`(约 23 倍)的周期尖刺,周期与 tick 一致。
|
|
171
|
+
- 修复:把「挂载时刻」用 ref 冻结(`phaseAtRef`),delay 只由它计算。这样
|
|
172
|
+
重渲染(含每秒 tick)不改变参数;重新挂载(切会话回来)时 ref 重取当前时刻,
|
|
173
|
+
相位仍按绝对时间对齐。
|
|
174
|
+
- 修复后实测:`--ss-bar-delay` 恒定(`delayStable: true`),增量极差
|
|
175
|
+
0.3336 → **0.0061**(缩小 55 倍),周期尖刺消失。
|
|
176
|
+
- 暂停 → 恢复仍正确重新锚定(10.33% → 10.72% 从冻结处继续,不归零)。
|
|
177
|
+
- **feat** 暂停态改为「原条 + 静止」(P1 定稿):**移除**原有的灰色斜纹填充。
|
|
178
|
+
- P1 原文(docs/ui/16):暂停 = 原条 + 静止,**不加任何纹理、不变色**。
|
|
179
|
+
状态由「动 / 静」对比表达:进行中进度条在推进,暂停时停住。
|
|
180
|
+
- 实现:暂停态只有 `animation: none`;进行中仅有推进动画
|
|
181
|
+
(`ss-dock-bar-progress`)。**不额外叠加流动/斜纹/冰封**——真机实测两条
|
|
182
|
+
的背景与底色完全一致(`background-image: none`、同为 `rgb(249,250,251)`),
|
|
183
|
+
只差动画在不在跑。
|
|
184
|
+
- 修正记录:先前实现误把「进行中加流光斜纹」当作 P1 的一部分(源于
|
|
185
|
+
设计探索阶段 G1 方案),但那不在 P1 定义内,已移除。教训:把**独立**的
|
|
186
|
+
设计决定捆进一个已拍板的方案里,会让"已确认"的范围被悄悄放大。
|
|
187
|
+
- 推进动画的参数仍由 JS 注入 `--ss-bar-duration / --ss-bar-delay`
|
|
188
|
+
(duration 固定为完整窗口、delay 为负的已过去量),CSS 负责声明动画,
|
|
189
|
+
JS 不写 `animationName` 简写以免覆盖。
|
|
190
|
+
- **feat** 暂停项支持「编辑文案」与「插话发送」(此前两者都被禁用,属实现缺口)。
|
|
191
|
+
- **编辑**:暂停项不在日志里(pause 时已 delete),旧实现必然 `schedule_not_found`,
|
|
192
|
+
客户端只好禁用按钮。现改为直接改写 sidecar 的 `PausedEntry.prompt`——不动时刻、
|
|
193
|
+
不动剩余秒数、不动 uid、不写会话事件,因此冻结进度与恢复后的窗口都不受影响。
|
|
194
|
+
- **插话发送**:语义是「不等倒计时立即推送」,对暂停项恰恰最需要。现从 sidecar 留档
|
|
195
|
+
取材构造消息(`user` 代发 / `context` 注入两形态与常规路径一致),投递成功后清掉留档
|
|
196
|
+
(提醒已送达、应出列)。
|
|
197
|
+
- 客户端:两处按钮解除 `disabled`;因宿主**不写会话事件**、投影不会立刻刷新,
|
|
198
|
+
补了本地文案覆盖(`promptOverrides`)与本地摘除复用(`dismissedPaused`),
|
|
199
|
+
待投影对账后自然失效。
|
|
200
|
+
- **fix** 暂停 / 恢复后进度条长度会变(**两处根因**):
|
|
201
|
+
- **窗口缩水(宿主)**:resume 用 `after_seconds = remaining` 重建日志记录,原窗口
|
|
202
|
+
另存 `ownership.windowSeconds`;而 pause 却从日志的 `afterSeconds` 取窗口,
|
|
203
|
+
于是每「恢复→再暂停」一轮窗口就缩水一次(2 分钟任务 → 暂停(120) → 恢复(85)
|
|
204
|
+
→ 再暂停(窗被记成 85)…)。进度条比例随之失真,实测塌缩到 2.3%。
|
|
205
|
+
修复:pause 优先取 `ownership.windowSeconds`,仅首次暂停回退日志值。
|
|
206
|
+
- **冻结帧基准(客户端)**:冻结比过去用 `scheduled_at` 反推,而暂停后该字段会
|
|
207
|
+
换成 `originalScheduledAt`,窗口基准整体跳变。改为纯算术
|
|
208
|
+
`(窗口 − 剩余) / 窗口`,并抽出 `src/bar-geometry.ts` 供两侧共用 + 单测覆盖。
|
|
209
|
+
- **教训**:中途曾用「客户端墙钟」做冻结上界,导致上界随时间前进、条越缩越短
|
|
210
|
+
(29% → 2.9%);现上界仅在宿主 `paused_at` 晚于点击时补回往返耗时,
|
|
211
|
+
且不影响主算式。`tests/bar-geometry.test.mjs` 锁死这两类回归。
|
|
212
|
+
- **fix** 删除暂停项后 dock 行不消失(**根因修复**):投影 view 改为**以 sidecar 为准**重读 `paused`,不再信任可能陈旧的 `state.paused` 镜像。
|
|
213
|
+
- 根因:删除暂停项只清 sidecar、不写会话事件——**也不能写**:该 id 在 pause 时已被
|
|
214
|
+
`dsh-schedule` 删除,补写 `delete` 会抛 `schedule delete targets inactive id`,让整份
|
|
215
|
+
日志读失败。
|
|
216
|
+
- 而投影 cell 只在事件到达时由 `apply` 推进,且会持久化进 projection cache;客户端又按
|
|
217
|
+
seq「更高者胜」消费控制帧 → 没有新事件就没有修正帧。宿主那份 `paused[]` 快照因此永久
|
|
218
|
+
陈旧,**刷新页面也会被缓存恢复**(此前表现为「删不掉、且刷新后复活」)。
|
|
219
|
+
- 修复:`viewUserScheduleProjection` 在 `sessionId` 可用时从 sidecar(留档的权威存储)
|
|
220
|
+
重读,`readFresh` 按 mtime/size 失效,删除即刻可见。旧快照不再可能让已删行复活。
|
|
221
|
+
- 客户端摘除相应收敛为「粘性直到投影变化」:仅当同 uid 以 active 复现(resume)才解除,
|
|
222
|
+
不再按时间过期——过期只会让陈旧投影里的已删行复活。
|
|
223
|
+
- **fix** resume 先清 sidecar `paused` 再 append `create`,消除「已暂停 + 新活动」双行。
|
|
224
|
+
- **fix** delete 支持 wire `uid`:活动任务先映射 scheduleId;暂停项直接清 sidecar(此前 `deleted:false`)。
|
|
225
|
+
- **feat** 移除右下角 ⏰ 到达 toast;到点以对话消息为准。
|
|
226
|
+
- **feat** 暂停 / 恢复(仅 `after` + 类提醒):dock 原地斜纹冻结,不拆分区。
|
|
227
|
+
- `/user-schedule-pause` / `/user-schedule-resume`;sidecar v2 存 `uid` + `paused` 草稿。
|
|
228
|
+
- 暂停 = 日志 `delete` + sidecar 留档;恢复 = `after_seconds=remaining` 重建(日志 id 会变,uid 不变)。
|
|
229
|
+
- 投影 `stateVersion: 4`,wire 带 `status` / `remaining_seconds`;非 after / 已到点不显示暂停。
|
|
230
|
+
- 进度条定稿 hatch(斜纹停放)。
|
|
231
|
+
- **chore** 上游跨代兼容收敛到 `src/upstream-compat.ts`:Session API 双轨探测
|
|
232
|
+
(`ownEvents` / `events` + `seedLength`)与 peer 代次矩阵(`KERNEL_GENERATIONS`)
|
|
233
|
+
单点维护。新 rc 落地:改矩阵 → `node scripts/sync-peer-matrix.mjs` → `npm test`。
|
|
234
|
+
- **fix** 0.1.1 路径补上 `header.seedLength` 回退(此前恒传 `inheritedEventCount=0`,
|
|
235
|
+
fork 子会话可能把继承前缀误算进用户任务)。
|
|
236
|
+
- 护栏:`tests/upstream-compat.test.mjs` 锁两代形状 + package.json peer 同步。
|
|
237
|
+
- **fix** 悬浮卡片现在反映定时状态(修复 #4)。此前侧栏行内 badge 显示
|
|
238
|
+
「有提醒」,而同一行悬浮卡片的状态区只说「空闲」,两者自相矛盾:卡片正文
|
|
239
|
+
由宿主的 `sessionStatuses` 硬编码状态链生成,不认识本插件的定时任务,且
|
|
240
|
+
宿主未暴露任何 hover 卡片插槽。
|
|
241
|
+
- 实现方式(纯插件,零宿主改动):在宿主已有卡片的状态区**追加**一行
|
|
242
|
+
与宿主 `.hoverStatus` 同构的状态行,内容为「N 条定时提醒 + 下次触发
|
|
243
|
+
时间」(到期走「等待发送」文案)。
|
|
244
|
+
- 关联方式:卡片是 `body` 级 portal,与会话行无 DOM 祖先关系,改用与行内
|
|
245
|
+
badge 同源的 React fiber 反查(`memoizedProps.node.id`)关联会话。
|
|
246
|
+
- **纯追加**语义:不改写、不替换、不重排宿主既有状态行,`pendingInteraction`
|
|
247
|
+
/ `running` 等状态优先级不变;无活动定时任务的会话卡片行为完全不变。
|
|
248
|
+
- 抗重渲染:卡片每次悬浮由 React 重建,依赖既有的 MutationObserver 重扫
|
|
249
|
+
重插,与 badge 同一机制。
|
|
250
|
+
- 探测不到宿主结构时安静跳过(绝不误标);不依赖任何构建期 hash 类名。
|
|
251
|
+
|
|
5
252
|
## 2026-09-12
|
|
6
253
|
|
|
7
254
|
- **chore!** 插件改名 `dsh-session-scheduler` → **`dsh-later`**:消除与核心
|
package/README.en.md
CHANGED
|
@@ -78,7 +78,7 @@ Conversations with active reminders show a pixel clock in their sidebar row (alo
|
|
|
78
78
|
| <img src="assets/icons/icon-presence-urgent.svg?v=18" width="28" alt="urgent"/> | Amber · next fire within 5 minutes |
|
|
79
79
|
| <img src="assets/icons/icon-presence-overdue.svg?v=18" width="28" alt="overdue"/> | Red · overdue (waiting for the session to become idle) |
|
|
80
80
|
|
|
81
|
-
Hover shows "
|
|
81
|
+
Hover shows "Next at 15:32 · 3 task(s)"; when overdue it shows "Due · 3 task(s) (sends when the session is idle)".
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
@@ -137,7 +137,14 @@ Hover shows "count · next fire time".
|
|
|
137
137
|
|
|
138
138
|
### Install into DSH
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
**From npm (recommended — prebuilt, no approval needed):**
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
dsh plugin --profile web add dsh-later
|
|
144
|
+
# restart dsh web to activate
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Or build from source and add it to your DSH Web profile:
|
|
141
148
|
|
|
142
149
|
```sh
|
|
143
150
|
cd dsh-later
|
|
@@ -184,6 +191,52 @@ Works out of the box. Adjust in **DSH Settings → Plugins → Session Scheduler
|
|
|
184
191
|
|
|
185
192
|
---
|
|
186
193
|
|
|
194
|
+
## For plugin developers: the schedule summary API (cross-plugin)
|
|
195
|
+
|
|
196
|
+
dsh-later exposes a workspace-wide schedule summary as a public data face
|
|
197
|
+
([issue #17](http://192.168.4.77:3000/dsh-plugins/dsh-later/issues/17)), so other client
|
|
198
|
+
plugins (e.g. dsh-group-status) can aggregate schedule state on folders/sidebar rows
|
|
199
|
+
**without** parsing the `userSchedules` projection themselves — and without drifting
|
|
200
|
+
from the inline row badge.
|
|
201
|
+
|
|
202
|
+
Declare the external module in `package.json` first:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{ "dsh": { "client": { "platform": "web", "external": ["dsh-later"] } } }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Then `require('dsh-later/client')` inside the client factory:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
const { getScheduleSummaryStore, summarizeScheduleGroup, useScheduleSummary } = require('dsh-later/client')
|
|
212
|
+
|
|
213
|
+
// 1) Read the store directly: session id → { count, pausedCount, nextAt, state }
|
|
214
|
+
const store = getScheduleSummaryStore() // undefined when dsh-later is not loaded
|
|
215
|
+
const summary = store?.getSnapshot() ?? new Map()
|
|
216
|
+
|
|
217
|
+
// 2) Aggregate per group (used by the folded-folder badge)
|
|
218
|
+
const group = summarizeScheduleGroup(summary, groupSessionIds)
|
|
219
|
+
// → { sessions, count, pausedCount, nextAt?, state? }
|
|
220
|
+
|
|
221
|
+
// 3) Subscribe inside a React component (re-renders if the store arrives late)
|
|
222
|
+
const live = useScheduleSummary()
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Field | Meaning |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `count` | Number of schedules (**including** paused ones) |
|
|
228
|
+
| `pausedCount` | Paused schedules (they count toward `count` but **not** toward `nextAt`) |
|
|
229
|
+
| `nextAt` | Earliest fire time (epoch ms); `undefined` when everything is paused |
|
|
230
|
+
| `state` | `scheduled` / `urgent` (≤5 min) / `overdue` (due but not dispatched), judged at snapshot time |
|
|
231
|
+
|
|
232
|
+
Contract notes:
|
|
233
|
+
|
|
234
|
+
- The summary contains **only sessions with schedules**; consumers look up their own `sessionIds`.
|
|
235
|
+
- The store is the **single source of truth**: dsh-later's own sidebar badge reads it too, so the
|
|
236
|
+
two can never disagree. It owns the `sessions.list` subscription, the 30s state tick and the
|
|
237
|
+
60s `session.list` read RPC (visible-page only).
|
|
238
|
+
- Consumers must tolerate `undefined` (dsh-later not installed/enabled) and degrade silently.
|
|
239
|
+
|
|
187
240
|
## Technical documentation
|
|
188
241
|
|
|
189
242
|
Developer- and maintainer-oriented content has moved out of this README:
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
| <img src="assets/icons/icon-presence-urgent.svg?v=18" width="28" alt="urgent"/> | 琥珀 · 下次触发 ≤ 5 分钟 |
|
|
79
79
|
| <img src="assets/icons/icon-presence-overdue.svg?v=18" width="28" alt="overdue"/> | 红 · 已到期(等待会话空闲派发) |
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
鼠标悬停可见「下次 15:32 · 3 条任务」;已到期时显示「已到期 · 3 条任务(等待会话空闲发送)」。
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
@@ -137,7 +137,14 @@
|
|
|
137
137
|
|
|
138
138
|
### 安装到 DSH
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
**npm 安装(推荐,预构建产物,无需授权):**
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
dsh plugin --profile web add dsh-later
|
|
144
|
+
# 重启 dsh web 后生效
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
或者从源码构建后装入 DSH Web profile:
|
|
141
148
|
|
|
142
149
|
```sh
|
|
143
150
|
cd dsh-later
|
|
@@ -184,6 +191,50 @@ dsh plugin --profile web add "file:/path/to/dsh-later"
|
|
|
184
191
|
|
|
185
192
|
---
|
|
186
193
|
|
|
194
|
+
## 给插件开发者:定时摘要 API(跨插件)
|
|
195
|
+
|
|
196
|
+
dsh-later 把「全会话定时任务摘要」作为公共数据面暴露出来
|
|
197
|
+
([issue #17](http://192.168.4.77:3000/dsh-plugins/dsh-later/issues/17)),其它客户端插件
|
|
198
|
+
(如 dsh-group-status)据此在分组/侧栏聚合显示定时状态,**无需**自己解析
|
|
199
|
+
`userSchedules` 投影,也不会与行内 badge 漂移。
|
|
200
|
+
|
|
201
|
+
消费方先在 `package.json` 声明外部模块:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{ "dsh": { "client": { "platform": "web", "external": ["dsh-later"] } } }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
然后在客户端 factory 里 `require('dsh-later/client')`:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
const { getScheduleSummaryStore, summarizeScheduleGroup, useScheduleSummary } = require('dsh-later/client')
|
|
211
|
+
|
|
212
|
+
// 1) 直接读 store:会话 id → { count, pausedCount, nextAt, state }
|
|
213
|
+
const store = getScheduleSummaryStore() // dsh-later 未加载时为 undefined
|
|
214
|
+
const summary = store?.getSnapshot() ?? new Map()
|
|
215
|
+
|
|
216
|
+
// 2) 按分组聚合(折叠分组徽标直接用它)
|
|
217
|
+
const group = summarizeScheduleGroup(summary, groupSessionIds)
|
|
218
|
+
// → { sessions, count, pausedCount, nextAt?, state? }
|
|
219
|
+
|
|
220
|
+
// 3) React 组件内订阅(store 迟到也会自动重渲染)
|
|
221
|
+
const live = useScheduleSummary()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
| 字段 | 含义 |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `count` | 任务数(**含**暂停项) |
|
|
227
|
+
| `pausedCount` | 暂停项数量(暂停项计入 count,但**不参与** `nextAt`) |
|
|
228
|
+
| `nextAt` | 最早触发时刻(epoch ms);全部暂停时为 `undefined` |
|
|
229
|
+
| `state` | `scheduled` / `urgent`(≤5 分钟)/ `overdue`(已到期未派发),按快照时刻判定 |
|
|
230
|
+
|
|
231
|
+
契约要点:
|
|
232
|
+
|
|
233
|
+
- 摘要**只包含有任务的会话**,无任务的会话不出现;消费方按自己的 `sessionIds` 查表即可。
|
|
234
|
+
- store 是**唯一数据权威**:dsh-later 侧栏 badge 也读它,因此两边必然同源;它内部负责
|
|
235
|
+
`sessions.list` 订阅、30s 状态 tick 与 60s `session.list` 读 RPC(仅页面可见时)。
|
|
236
|
+
- 消费方必须容忍 `undefined`(dsh-later 未安装/未启用时静默降级)。
|
|
237
|
+
|
|
187
238
|
## 技术文档
|
|
188
239
|
|
|
189
240
|
以下内容面向开发者与维护者,已移出本 README:
|
package/icon.svg
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="18" cy="18" r="11.5" fill="none" stroke="url(#a)" stroke-width="2.6" stroke-linecap="round"/>
|
|
3
|
+
<path d="M18 10.8V18L23.6 21.4" stroke="url(#a)" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="a" x1="18" y1="6.5" x2="18" y2="29.5" gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop stop-color="#7AC2FF"/>
|
|
7
|
+
<stop offset="1" stop-color="#4A65E8"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进度条几何:活动期与暂停冻结期**共用同一套时间基准**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要单独成模块并写清楚:进度条在「活动 ↔ 暂停 ↔ 恢复」之间切换时,
|
|
5
|
+
* 稍有不一致就会让用户看到条长跳变。历史上有两类真实缺陷:
|
|
6
|
+
*
|
|
7
|
+
* 1. 两套基准:冻结比曾由 `remaining_seconds`(整数秒、ceil)反推,与活动期
|
|
8
|
+
* 精确 ms 的 `now` 恒差一个取整 → 非整秒暂停必然变长。
|
|
9
|
+
* 2. 两个时刻:`paused_at` 是**宿主**处理暂停命令的时刻,比用户点下暂停时最后
|
|
10
|
+
* 渲染的那一帧晚(命令通道往返,实测约 0.3–0.5s)。直接用它做冻结帧,条长
|
|
11
|
+
* 会比前一帧更长 → 「点暂停,条忽然变长」。
|
|
12
|
+
*
|
|
13
|
+
* 现在统一为:`barWindow()` 求窗口,冻结帧只把 `now` 换成冻结时刻,并把冻结
|
|
14
|
+
* 时刻**钳到 ≤ 客户端已渲染时刻**。因此暂停瞬间条长与点击前严格一致。
|
|
15
|
+
*
|
|
16
|
+
* 本模块被 host 与 client 两侧共用(与 command-outcome / smart-window /
|
|
17
|
+
* time-utils 同款:同时列出在 tsconfig.json 与 tsconfig.client.json 的 include 中)。
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-later/bar-geometry
|
|
20
|
+
*/
|
|
21
|
+
/** 每根进度条代表的时长(1 小时 = 一根满管)。 */
|
|
22
|
+
export const BAR_UNIT_MS = 3_600_000;
|
|
23
|
+
/**
|
|
24
|
+
* 推导进度条起点与总窗口(暂停/活动期共用同一套时间基准)。
|
|
25
|
+
* - `after`:`window_seconds`(resume 后保留的原间隔)→ `after_seconds` → `created_at` → 首见时刻
|
|
26
|
+
* - `every`:`every_seconds` 反推
|
|
27
|
+
* - 其余:`created_at` → 首见时刻
|
|
28
|
+
*/
|
|
29
|
+
export function barWindow(item, end, now, firstSeen) {
|
|
30
|
+
let start;
|
|
31
|
+
if (item.kind === 'after') {
|
|
32
|
+
// resume 后 after_seconds=剩余;window_seconds=原间隔 → 进度从冻结处继续
|
|
33
|
+
const windowSec = item.window_seconds ?? item.after_seconds;
|
|
34
|
+
if (windowSec !== undefined) {
|
|
35
|
+
start = end - windowSec * 1000;
|
|
36
|
+
}
|
|
37
|
+
else if (item.created_at !== undefined) {
|
|
38
|
+
const parsed = Date.parse(item.created_at);
|
|
39
|
+
start = Number.isNaN(parsed) ? (firstSeen.get(item.id) ?? now) : parsed;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
start = firstSeen.get(item.id) ?? now;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (item.kind === 'every' && item.every_seconds !== undefined) {
|
|
46
|
+
start = end - item.every_seconds * 1000;
|
|
47
|
+
}
|
|
48
|
+
else if (item.created_at !== undefined) {
|
|
49
|
+
const parsed = Date.parse(item.created_at);
|
|
50
|
+
start = Number.isNaN(parsed) ? (firstSeen.get(item.id) ?? now) : parsed;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
start = firstSeen.get(item.id) ?? now;
|
|
54
|
+
}
|
|
55
|
+
return { start, totalMs: Math.max(1000, end - start) };
|
|
56
|
+
}
|
|
57
|
+
/** 在给定时刻求进度比与分段数(elapsed 钳在 [0, total])。 */
|
|
58
|
+
function ratioAt(item, at, firstSeen) {
|
|
59
|
+
const end = Date.parse(item.scheduled_at);
|
|
60
|
+
const { start, totalMs } = barWindow(item, end, at, firstSeen);
|
|
61
|
+
const elapsedMs = Math.min(Math.max(at - start, 0), totalMs);
|
|
62
|
+
return {
|
|
63
|
+
totalRatio: elapsedMs / totalMs,
|
|
64
|
+
count: Math.max(1, Math.ceil(totalMs / BAR_UNIT_MS)),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** 活动期进度比:elapsed / total。 */
|
|
68
|
+
export function barSegments(item, now, firstSeen) {
|
|
69
|
+
return ratioAt(item, now, firstSeen);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 暂停冻结进度比。
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ 关键:**不能用 `scheduled_at` 反推**。暂停后 wire 的 `scheduled_at` 会从
|
|
75
|
+
* 「resume 后的新目标」换成 `originalScheduledAt`(原目标),而 `window_seconds`
|
|
76
|
+
* 始终是原窗口。若沿用活动期那套 `end - window` 的算法,暂停瞬间窗口基准整体
|
|
77
|
+
* 跳变,条长会剧烈塌缩(实测 25.7% → 3.3%)。
|
|
78
|
+
*
|
|
79
|
+
* 因此冻结帧改成**自洽的纯算术**,不依赖任何可能变义的字段:
|
|
80
|
+
* ratio = (window - remaining) / window
|
|
81
|
+
* 其中 window 取 `window_seconds ?? after_seconds`,remaining 取
|
|
82
|
+
* `max(0, frozenLeftMs)`(即 `remaining_seconds * 1000`)。
|
|
83
|
+
*
|
|
84
|
+
* 这样冻结比例天然等于「暂停那一刻已经过掉的部分」,与暂停前后用的是同一个
|
|
85
|
+
* 窗口;`paused_at` 只用于可选的上界钳制(见 `renderedAt`)。
|
|
86
|
+
*
|
|
87
|
+
* @param frozenLeftMs - 冻结剩余毫秒(`remaining_seconds * 1000`)。
|
|
88
|
+
* @param renderedAt - 可选:客户端已渲染过的最新时刻上界;提供且早于暂停时刻时,
|
|
89
|
+
* 按该上界折算,保证冻结帧不超过用户已看到的进度。
|
|
90
|
+
*/
|
|
91
|
+
export function frozenBarSegments(item, frozenLeftMs, firstSeen, renderedAt) {
|
|
92
|
+
const windowSec = item.window_seconds ?? item.after_seconds;
|
|
93
|
+
if (windowSec === undefined || windowSec <= 0) {
|
|
94
|
+
// 兜底:信息不足时退回按 scheduled_at 推算(旧行为)
|
|
95
|
+
const end = Date.parse(item.scheduled_at);
|
|
96
|
+
const pausedAt = Date.parse(item.paused_at ?? '');
|
|
97
|
+
const exact = Number.isNaN(pausedAt)
|
|
98
|
+
? end - Math.floor(frozenLeftMs / 1000) * 1000
|
|
99
|
+
: pausedAt;
|
|
100
|
+
return ratioAt(item, Number.isNaN(exact) ? end : exact, firstSeen);
|
|
101
|
+
}
|
|
102
|
+
const totalMs = Math.max(1000, windowSec * 1000);
|
|
103
|
+
// 剩余时间不得为负;也不得超过窗口(避免脏数据把条拉爆)
|
|
104
|
+
let leftMs = Math.min(Math.max(frozenLeftMs, 0), totalMs);
|
|
105
|
+
// 上界钳制:宿主 `paused_at` 是命令到达时刻,比用户点击晚(往返耗时 Δ)。
|
|
106
|
+
// 若提供了「客户端已渲染时刻」且 paused_at 更晚,则把冻结剩余**补回** Δ,
|
|
107
|
+
// 使冻结帧等于暂停前最后一帧,而不是凭空多走一截。
|
|
108
|
+
const pausedAt = Date.parse(item.paused_at ?? '');
|
|
109
|
+
if (renderedAt !== undefined && !Number.isNaN(pausedAt) && pausedAt > renderedAt) {
|
|
110
|
+
leftMs = Math.min(leftMs + (pausedAt - renderedAt), totalMs);
|
|
111
|
+
}
|
|
112
|
+
const elapsedMs = Math.min(Math.max(totalMs - leftMs, 0), totalMs);
|
|
113
|
+
return {
|
|
114
|
+
totalRatio: elapsedMs / totalMs,
|
|
115
|
+
count: Math.max(1, Math.ceil(totalMs / BAR_UNIT_MS)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 依据窗口与当前时刻推导动画计划。
|
|
120
|
+
*
|
|
121
|
+
* @param totalMs - 总窗口毫秒(> 0)。
|
|
122
|
+
* @param elapsedMs - 已过去毫秒(钳到 [0, totalMs])。
|
|
123
|
+
* @returns 计划;`totalMs <= 0`、非有限值、或已到点(剩余为 0)时返回 `undefined`。
|
|
124
|
+
*/
|
|
125
|
+
export function planBarAnimation(totalMs, elapsedMs) {
|
|
126
|
+
if (!Number.isFinite(totalMs) || totalMs <= 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
const raw = Number.isFinite(elapsedMs) ? elapsedMs : 0;
|
|
129
|
+
const clamped = Math.min(Math.max(raw, 0), totalMs);
|
|
130
|
+
const elapsed = clamped === 0 ? 0 : clamped;
|
|
131
|
+
// 已到点:没有可推进的时长,退回静态宽度(100%)
|
|
132
|
+
if (elapsed >= totalMs)
|
|
133
|
+
return undefined;
|
|
134
|
+
return {
|
|
135
|
+
durationMs: totalMs,
|
|
136
|
+
// -0 会在 CSS 里序列化成难看的 "-0ms",故显式归零
|
|
137
|
+
delayMs: elapsed === 0 ? 0 : -elapsed,
|
|
138
|
+
};
|
|
139
|
+
}
|