Releases: LMG-arch/work-time
Release list
v3.17.44 — 通知可靠性修复(ID 重复)+ 通知自检升级
本次修复:通知到时间不响
排查方式(无真机也能验证)
在浏览器注入 Capacitor 插件 mock,完整跑通 Android 调度分支(真机实际执行的那条路径),断言渠道创建 / 调度条数 / exact / allowWhileIdle / extra.kind。
结论:v3.17.43 的调度逻辑本身是正确的(0 错误、3 个渠道、56 条通知、exact:true)。
发现并修复的真实缺陷
通知 ID 重复:旧算法 = 秒级基准 + 自增计数 + 随机数(0~999),计数与随机数会互相抵消。
- 实测批量调度 56 条时出现重复 ID
- 重复 ID 会让后一条覆盖前一条 → 部分提醒静默不响
- 已改为「秒级基准 + 自增序号」,复测 56 条 ID 全部唯一
自检升级(重点)
真机看不到 console,所以「设置 → 🔍 诊断通知问题」现在直接给出完整证据,一次复制即可定位:
===== 上班日历 · 通知自检 =====
版本: 3.17.44 (versionCode 81)
环境: Android (Capacitor)
精确闹钟模式(本会话): ✅ 精确 exact / ⚠️ 非精确 inexact
提醒数据: 4 条(启用 4 条)
✅ LocalNotifications 插件已加载
✅ 通知权限: granted
❌ 精确闹钟权限: denied → 系统设置→应用→上班日历→闹钟和提醒,开启后重启 App
渠道 clockin-reminders: importance=5 sound=default
📋 待发送通知: 56 条(clockin:56)
最近调度[clock-in]: 2026/9/14 07:48 生成56 调度56 丢弃0 剩余配额397
怎么读:
待发送通知: 0 条→ 系统里没排上闹钟(打开 App 保持前台 10 秒会自动重排 14 天)精确闹钟权限: denied→ 只能走 inexact,Doze 下会「到点不响」→ 按提示开启后重启 App通知权限: denied→ 系统设置里开启渠道 importance<5 或 sound=null→ 通知渠道被系统/用户降级,需在系统设置里改回
若仍不响,问题在设备侧
请把上面的自检报告发来。最常见的三项:
- 系统设置 → 应用 → 上班日历 → 通知:允许通知
- 系统设置 → 应用 → 上班日历 → 闹钟和提醒:开启(精确闹钟)
- 系统设置 → 应用 → 上班日历 → 电池:改为「不受限制」+ 允许「自启动」(国产 ROM 必须)
提示:升级后旧版本的崩溃记录仍会弹一次(存在手机 crashlogs 目录里),点「清除并关闭」即可。
v3.17.43 — 紧急修复 v3.17.42 崩溃 + 通知调度迁移
⚠️ 紧急修复:v3.17.42 崩溃
v3.17.42 精简 window 垫片时删除了 isCapacitorPlatform 等绑定,但多个模块仍以裸名调用(foo() 等价于 window.foo()),导致真机报「未处理的 Promise 拒绝: isCapacitorPlatform is not defined」。
受影响链路:通知调度、会话恢复、统计导出图片、更新检查。请尽快升级。
修复内容
崩溃修复(P0)
- 修复垫片裁剪导致的裸名未定义崩溃(通知调度 / 会话恢复 / 统计导出 / 更新检查)
- 顺带修复:
renderer.js启动就绪门仍在等待已删除的renderCalendar→ v3.17.42 每次启动空等 5 秒才加载数据 - 顺带修复:点击通知跳转、确认打卡后刷新 的死引用改用 Vue 钩子(
__vueActivate/__refreshCalendarGrid) - 顺带修复:
reminderNotifTimer从未声明(严格模式下抛错,Web 端轮询兜底实际从未生效)
重构 S4.2 · 通知调度迁移(调度算法零改动)
- 新增
src/lib/notifications.js:打卡/待办预调度、精确闹钟配额守门、测试通知、诊断 src/reminders/reminders.js733 → 48 行,只保留数据函数
全模块显式 import
notifications/stats/updater/social/renderer补齐显式导入,彻底摆脱 window 垫片supabase/client.js对getProfile用动态 import 破除循环依赖
验证
- vite 构建通过
- 通知调度回归:
scheduleReminderNotifications()/scheduleTodoReminders()均正常、0 错误(此前必崩) - 日历 / 打卡 / 好友圈 / 统计 / 设置 视图遍历 0 错误
- 新增裸名审计(
barename-audit.cjs)并纳入回归矩阵,后续裁剪垫片必须零残留
v3.17.42 重构阶段4 · 经典层下线
v3.17.42 重构阶段 4 · 经典层下线
依据 docs/重构计划-2026-09-11.md 执行 S4.3/S4.4/S4.5/S4.6(S4.1 已随 v3.17.41 发布)。
改动一览
| 文件 | 前 → 后 | 说明 |
|---|---|---|
src/index.html |
341 → 143 行 | 删除 .app 死壳(含 59 个 id 的 DOM)与 .toolbar(已被 body.life-mode 永久隐藏,Vue 外壳侧边栏是唯一导航) |
src/renderer.js |
689 → 190 行 | 只留三类职责:启动引导 / syncToWindow 桥 / 原生事件(onReminderConfirmed、onDataChanged);60 处死壳 DOM 绑定 → 0 |
src/shims.js |
148 → 65 绑定 | 按 stage4b-audit.cjs 的真实消费者扫描逐条裁剪;window.refreshAllData 改绑 dataService(social.js 裸名消费方零改动) |
src/calendar/calendar.js |
260 → 37 行 | 删除 renderCalendar/createDayCell/openDetailPanel 等渲染函数,保留 getHolidayInfo/loadAllData/loadHolidays |
src/todos/todos.js |
106 → 41 行 | 保留 loadTodos/getTodosForDate/isTodoDone 数据内核 |
src/settings/settings.js |
190 行 → 删除 | 全部为死壳 DOM 代码(SettingsPage.vue 已有本地实现) |
验证(agent-browser 实测)
- 冷启动:life-app 挂载、Vue 外壳 571 个可见元素、0 未捕获错误
- 6 视图遍历:日历 35 格 / 打卡 24 提醒元素 / 社交 66 元素 / 统计 / 设置 / 生活工作台 全通过
- 数据链路:saveDay → store → UI 同步(单元格即时显示「班」);getHolidayInfo('2026-10-01')=国庆节
- 全局 window.onerror = 0
阶段 5 收尾(下一步)
通知调度迁入 src/lib/notifications.js(S4.2,算法零改动)→ social.js 渲染函数清理 → updater 弹窗组件化 → shims 最终退役评估。
v3.17.41 重构阶段3b/4a · 数据服务迁移
v3.17.41 重构阶段 3b/4a · 数据服务迁移
依据 docs/重构计划-2026-09-11.md 推进(S4.1 + S3.1c)。
| 改动 | 说明 |
|---|---|
src/lib/dataService.js |
新增 Vue 侧数据刷新唯一入口:拉取日历/待办/提醒/记账数据 → 写入 Pinia store → 触发纯重渲染钩子 → 过渡期继续发布 window 镜像(经典层兼容) |
calendarStore |
不再从 window.allData 灌数据;syncFromWindow() 改为直接从原生桥重新拉取 |
| 刷新钩子 | __refreshCalendarGrid / __refreshStats 改为纯重渲染(不再隐式读 window 镜像;保存后 store 自身状态已最新) |
| 设置页 | 6 处 window.refreshAllData 调用改走 dataService.refreshAllData()(同步/下载/导入/恢复路径全覆盖) |
| 诊断出口 | 新增 window.__dataService.refreshAllData(),便于排查"同步后不刷新"类问题 |
验证(agent-browser 实测)
- 冷启动:Pinia
daysData有数据、Vue 网格正常渲染、0 错误 - 保存链路:打卡 → 状态写入 → store 与界面同步更新(纯 tick 刷新)
dataService.refreshAllData()端到端:before/after 数据一致、单元格状态正确、0 错误
死壳审计结论(决定阶段 4 策略)
.app 死壳 59 个 id 中 48 个仍被经典脚本引用 → 删除 DOM 必须与经典渲染层删除同批进行,已记入计划 §9。
下一步
经典渲染层分模块退役 → 同批删除 .app 死壳 → 收窄 shims.js → 阶段 5 收尾。
v3.17.40 重构阶段3(第一批) · Vue 去 window 数据镜像
v3.17.40 重构阶段 3(第一批)· Vue 去 window 数据镜像
依据 docs/重构计划-2026-09-11.md 阶段 3 执行(第一批)。
| 步骤 | 改动 |
|---|---|
| S3.1a | todoStore:初始状态不再取 window.allTodos(改 ref([]),数据由 loadTodos() 经原生桥加载);isDone() 改查自身 todos;deleteTodo()/updateTodo() 改为先更新自身状态再发布到 window(过渡期兼容) |
| S3.1b | reminderStore:reminders/reminderRecords 初始为空;refreshFromWindow() / refreshReminderList() / refreshReminderHistory() 增加类型守卫,不再把 window 当数据源 |
| S3.2a | TodoModal:农历换算改为 import { lunarToSolar } from '../utils.js';修复隐患——文件内原有同名本地包装函数,直接 import 同名会造成自我递归,改为 import 原名 + 本地薄封装 lunarDateToSolar |
原则:Vue 不再读 window 镜像;仍保留写(经典层未下线,属过渡期兼容,阶段 4 一并移除)。
验证(agent-browser 实测)
window.allReminders仍被 store 加载后发布(4 条),过渡期兼容未破坏- 打卡页:喝水 8 杯、提醒相关元素 24、待办行 4 → 列表渲染正常
- 日历页:Vue 网格 35 格(+ 隐藏经典 35 格);记账页占比条 4 段;总览时间线 5 行
- 全流程
window.onerror= 0
顺序修订(重要)
执行中发现 S3.1c 反向依赖 S4.1:window.allData 由经典层 calendar.js loadAllData() 产出,只有先把数据服务迁到 Vue 侧才能去掉读取。故 S3.1c + S3.2b + S3.3 与 S4.1 合并推进(详见计划文档 §9)。
下一步(阶段 3 第二批 / 阶段 4 起点)
src/lib/dataService.js(迁移 loadAllData/refreshAllData)→ 去掉 calendarStore/StatsPage 对 window.allData 的读取 → CalendarView/StatsPage 直连 holidays → src/lib/bridge.js 统一 calendarAPI → exportStatsAsImage 迁移 → 删除 .app 死壳与经典渲染层 → 收窄 shims.js。
v3.17.39 重构阶段2 · lifeEngine 稳健化
v3.17.39 重构阶段 2 · lifeEngine 稳健化
依据 docs/重构计划-2026-09-11.md 阶段 2 执行。
S2.1 统一 DOM 写入助手
新增(lifeEngine 模块内):needEl / $el / setText / setHtml / setProp / setStyle / classOp / focusFormField
—— 宿主缺失时只告警一次([life] 缺少元素 #id(写入已跳过)),绝不抛错。
S2.2 裸写全量替换(69 处 → 0)
教训:首版用正则
([^;]+)匹配赋值表达式,被箭头函数体内的;截断(recentRecords一行),导致 vite/rolldown 构建报 PARSE_ERROR,而node --check未捕获。改为括号/字符串深度感知扫描器后一次通过;并新增.mjs语法校验(模块目标解析,比node --check严格)。
| 类型 | 处数 |
|---|---|
textContent / innerHTML / value / disabled / hidden / checked / src / href |
66 |
style.* |
2 |
classList.* |
1 |
| 合计 | 69 |
S2.3 失败隔离
renderAll():10 个渲染函数各自try/catch,单模块异常不再中断整条渲染链init()绑定阶段:bindForms/bindEvents/initFormPanelToggles各自隔离- 弹层自动聚焦改走
focusFormField()(原先 setTimeout 内裸取.elements.xxx.focus())
验证(agent-browser 实测)
- 7 个生活视图(总览/记账/习惯/减脂/周计划/家务/书影音/档案)遍历:0 未捕获错误、0 告警
- 失败隔离注入测试:强行移除
#homeList#habitProgress#moneyFilter#archiveList后触发两次renderAll
→ 4 条精确告警 + 0 未捕获错误 + 其余模块照常渲染(时间线 5 行、饼图占比 4 段、Hero 文案正常)
(改动前:任一元素缺失即抛错并中断整条渲染链、弹致命浮层) - 构建:
vite build ✓ 14.6s;ESM 语法校验 ✓
下一步
阶段 3(v3.17.40):Vue 侧去 window 数据镜像与能力直连(holidays/lunar/supabase/原生桥)。
v3.17.38 重构阶段1 · 存储与 API 单真值
v3.17.38 重构阶段 1 · 存储与 API 单真值
依据 docs/重构计划-2026-09-11.md 阶段 1 执行。
| 步骤 | 改动 | 结果 |
|---|---|---|
| S1.1 | 主题单一真值:appStore 在创建时应用主题(接管冷启动);删除经典 settings.js setTheme/loadTheme、shims.js 两处桥接、renderer.js 的 loadTheme 调用块 |
主题唯一写入者 = appStore(键 calendar-theme) |
| S1.2 | 导航设置单一真值:删除 renderer.js 经典导航设置 IIFE(55 行,目标按钮位于死壳内) |
唯一真值 = calendar-nav-items(LifeWorkbench / SettingsPage 管理) |
| S1.3 | lifeEngine.loadState 存储读写配对:优先 __storage.get(与 saveState 的 set 成对),旧数据回退 getRaw → localStorage |
既有数据不迁移、不丢失 |
| S1.4 | reminders.js 裸全局 showToast(...) → window.showToast?.(...) |
与统一提示入口对齐 |
验证(agent-browser 实测)
- 冷启动主题:写入
calendar-theme=purple→ 重载 →body.dataset.theme=purple;区域 A--accent= 区域 B--plum=#7e57c2;0 错误 - 主题持久化:切回 cosmic 后重载保持
- 生活数据加载(新
.get路径):records 24 / habits 5 / 总览时间线 5 行;saveText=已自动保存 - 导航设置(IIFE 删除后):
calendar-nav-items去掉stats→ 重载 → 子导航日历/打卡/好友/设置(无统计)、侧边栏[data-work-sub=stats]display:none - 记账回归:占比条 4 段 + 图例百分比(购物66%/娱乐19%/吃饭13%/交通2%)
- 全流程
window.onerror捕获 0 条
附带产出
docs/重构回归清单.md:R1–R12 用例、通用探针、冒烟铁律(含双网格陷阱:必须限定.calendar-view .day-cell)、阶段 0 基线快照- 基线标签
refactor-base(= v3.17.37)
下一步
阶段 2(v3.17.39):lifeEngine 稳健化 —— 统一 DOM 写入助手 + 69 处裸写分区替换 + 渲染函数失败隔离。
v3.17.37 两区域融合:提示/主题/数据/渲染统一
v3.17.37 两个区域融合:提示 / 主题 / 数据 / 渲染统一
融合项(已完成 5 项)
| # | 融合项 | 融合前 | 融合后 |
|---|---|---|---|
| 1 | 提示机制 | 两套实现、两个宿主(#toast / #legacy-toast),配色/位置/时长各异 |
唯一实现 + 唯一宿主 #toast;utils.showToast 委托 window.__lifeToast;统一样式(--toast-bg 随主题 / bottom 92px / 2400ms) |
| 2 | 主题令牌 | 生活区固定暖纸色,仅 dark/cosmic 被覆盖 | .life-app 令牌全部由主题令牌派生,15 套主题下两区域同色系联动 |
| 3 | 喝水记录 | Vue 留空容器、经典脚本注入并直写 water-records(双写入者) |
ClockinPage.vue 响应式自渲染,reminderStore 唯一写入者;删除经典 4 函数 + 4 处 window.* 桥接 |
| 4 | 日历渲染 | 9 处 Vue 组件反向调用经典 renderCalendar()(重复渲染隐藏 DOM) |
全部移除,统一走 Vue 自身 __refreshCalendarGrid |
| 5 | 失效调用 | 收口后遗留 5 处 renderClockinView() 调用(ReferenceError 风险) |
改为 Vue 刷新钩子(renderer ×3 / reminders / calendar) |
验证
- 提示:
showToast()→#toast、无#legacy-toast、bg=主题色、bottom 92px/radius 20px/z-index 1200 - 主题:green 下区域 A
--accent= 区域 B--plum=#2e9e4f;cosmic 无回归 - 喝水:8 杯渲染、点 + → 1/8 杯、
water-records落库、0 错误 - 日历:35 格渲染、标记「上班」→ store
work+ 格子显示「班」、0 错误 - 发布包校验:
__lifeToast存在、legacy-toast为 0、water-cups存在、renderClockinView为 0
全项目审查
报告见 docs/融合与一致性审查报告-2026-09-11.html:P0 3 项(经典死壳 DOM / lifeEngine 约 98 处无守卫写入 / 经典-Vue 双向耦合)、P1 5 项(双网格、存储双写 ×2、存储 API 不配对、图表配色)、P2 4 项(死函数与注释、裸全局调用、样式遗留),并给出三阶段调整顺序。
v3.17.36 根治删除记录崩溃(showToast 误删 #toast 宿主)
v3.17.36 根治:删除记录时 "Cannot set properties of null (setting 'textContent')"
完整调用链(已逐帧核对 v3.17.32 的 bundle)
文字记录:
Uncaught TypeError: Cannot set properties of null (setting 'textContent')
at nM (vue-main-Drr0pX4a.js:410:33536) ← toast()
at sM (…:410:35149) ← deleteRecord()
at HTMLDocument.<anonymous> (…:410:76219) ← 文档级 click 委托(a==='delete'&&sM(i))
按行列号切片核对确认:nM = lifeEngine toast();sM = deleteRecord();第三帧 = [data-action="delete"] 委托分支。
真正的根因:两套 toast 互相踩踏
src/life/markup.html 里生活工作台的提示宿主是:
<div aria-live="polite" class="toast" id="toast" role="status"></div>而工作模块(待办 / 提醒 / 好友 / 设置 / 备注…)走 src/utils.js 的另一套 showToast(),原实现是:
const existing = document.querySelector('.toast'); // ← 命中的正是 life 的 #toast(class 同为 toast)
if (existing) existing.remove(); // ← 把它永久删除
const toast = document.createElement('div');
toast.className = 'toast'; // ← 自建元素没有 id
document.body.appendChild(toast);
… setTimeout(() => toast.remove(), 300) // ← 2.1 秒后自建元素也消失于是:工作模块弹过一次提示后,#toast 就从 DOM 里彻底消失;此后生活工作台任何带提示的操作(删除记录等)调用 getElementById('toast') 得到 null → 直接写 textContent → 崩溃并弹出致命浮层。
这也解释了截图现象:底栏停在「工作」(刚在工作模块触发过提示,如「打卡成功 ✓」),弹出的却是生活工作台操作引发的错误。
逻辑自洽性验证:
deleteRecord()在toast()之前会先跑renderAll(),而renderAll()对#brandAvatar等元素是无保护的写入。既然错误发生在toast()而不是renderAll(),说明 markup 整体完好、只有#toast这一个元素被摘掉了——与上述机理完全一致。
修复
| 文件 | 改动 |
|---|---|
src/utils.js |
showToast() 只清理自建的 #legacy-toast(并给它加 id),不再用 querySelector('.toast') 误删其他宿主 |
src/life/lifeEngine.js |
新增 ensureOverlayHost():#toast/#confetti 缺失时自动重建(挂 .life-app,无则挂 body + 内联样式)——即使宿主被外力删除也能自愈 |
src/life/lifeEngine.js |
文档级 click 委托整体 try/catch([life] action failed:),单点异常不再升级为致命浮层 |
src/life/lifeEngine.js |
initLife() → initWhenReady() 有限重试(8×120ms),界面未就绪不再崩 |
验证(agent-browser 实测)
- 调用
window.showToast('测试提示')→ life 的#toast仍在,只产生#legacy-toast;3 秒后 legacy 自动消失、#toast完好 - 完整复现原路径:先
showToast('打卡成功 ✓')(工作模块)→ 切到生活工作台 → 新增并删除一条记录 → 提示仍显示「记录已删除」、0 未捕获错误、无致命浮层 - 移除
#toast后删除记录 → 宿主自动重建、提示正常 - 移除
#confetti后庆祝路径 → 彩带自动重建 - 处理器内部异常(故意移除
#brandAvatar)→ 仅[life] action failed:告警
安装提示
崩溃日志的 bundle 为 vue-main-Drr0pX4a.js,对应 v3.17.32 安装包。本版(v3.17.36)同时包含 v3.17.33 / v3.17.34 / v3.17.35 的全部修复。
v3.17.35 崩溃修复:删除记录时 textContent of null
v3.17.35 崩溃修复:删除记录时 "Cannot set properties of null"
问题
安卓端(v3.17.32)删除一条记录后弹出致命错误浮层:
Uncaught TypeError: Cannot set properties of null (setting 'textContent')
at nM (vue-main-Drr0pX4a.js:410:33536) ← toast()
at sM (…:410:35149) ← deleteRecord()
at HTMLDocument.<anonymous> ← 文档级 click 委托
根因
deleteRecord() 末尾调用 toast('记录已删除'),而 toast() 原实现无任何空值保护:
function toast(message){const el=document.getElementById('toast');el.textContent=…}#toast 取不到时直接写 textContent 抛错,被全局错误处理器渲染成致命浮层。同类隐患还有 celebrate() 的 #confetti,以及 init() 中大量无保护的 getElementById(...).textContent。
修复(src/life/lifeEngine.js)
| 位置 | 改动 |
|---|---|
新增 ensureOverlayHost(id, class, fallbackStyle) |
宿主元素缺失时动态创建并挂到 .life-app(无 .life-app 时挂 body 并注入内联样式),返回 null 前不抛错 |
toast() |
走 ensureOverlayHost,宿主建不出来时降级为 console.warn 而不崩溃 |
celebrate() |
同上(#confetti 自建) |
| 文档级 click 委托 | 整体包 try/catch:单个动作失败 → console.warn('[life] action failed: …'),不再升级为未捕获错误/致命浮层 |
initLife() |
改为 initWhenReady(0):markup 未就绪时有限重试(8×120ms),最终未就绪则明确告警并跳过,避免 getElementById 返回 null 抛错 |
验证(agent-browser 实测)
- 删除记录 + 移除
#toast(复现用户场景)→ 提示宿主自动重建、文案「记录已删除」正常显示、0 未捕获错误、无致命浮层 - 移除
#confetti+ 勾选任务(庆祝路径)→ 彩带自动重建(16 片)、无错误 - 无
.life-app的降级路径 → toast 挂到 body + 内联样式,仍可见、无错误 - 处理器内部抛错(故意移除
#brandAvatar后删除)→ 仅[life] action failed: …告警,无浮层
说明
崩溃上报的 bundle 为 vue-main-Drr0pX4a.js,对应 v3.17.32 的安装包;本版同时包含 v3.17.33(主题颜色统一)与 v3.17.34(消费结构占比条)的修复,建议直接安装 v3.17.35。