dsh
OpenPetsHQ/openpets将 DeepSeek Harness 的生命周期状态、错误与审批请求,桥接到本地运行的 OpenPets 桌面伙伴。
DeepSeek Harness 提醒 + 任务计时插件:提醒——按累计运行间隔定时提醒、超过计划用时发超时提醒,模型需要你确定或选择时发红色常驻提醒(人在 DSH 里走界面提醒卡 + 提示音,不在 DSH 里发系统通知;卡片滞留时长可配置、可设为常驻不自动消散,点击卡片可跳转所属会话);任务计时——为每个会话设置计划用时,仅模型运行时连续计时,任务结束自动统计实际用时与计划的差距,任务面板支持点击跳转会话,本地记录持久化。
提醒 + 任务计时 — DeepSeek Harness 插件,主要就做两件事:
- 🔔 提醒 — 按累计运行间隔提醒、超时提醒、需要你决策时的红色常驻提醒;人在 DSH 里走界面提醒卡 + 提示音,不在 DSH 里发系统桌面通知
- ⏳ 任务计时 — 为每个会话设置计划用时,只计模型运行时间,任务结束自动统计实际用时与计划的差距
两个主功能:🔔 提醒 · ⏱ 任务计时。 下面的清单按这两个主功能分组,第三节的配套能力是为了让这两件事更好用、数据不丢。
| 能力 | 描述 |
|---|---|
| ✅ 定时提醒 | 按累计运行时间间隔提醒(间隔在设置弹窗里填,默认值可在设置页改) |
| ✅ 超时提醒 | 超过计划用时自动发出超时提醒 |
| ✅ 决策提醒 | 模型需要你确定 / 选择方案时,红色「需要你决策」提醒(界面内常驻卡;不在 DSH 里时额外发系统桌面通知) |
| ✅ 内部通知 / 外部通知分流 | 人在 DSH 里只发内部通知(提醒卡 + 界面内提示音),人不在 DSH 里才发系统通知,两边不会同时响;「外部通知时机」可配置 |
| ✅ 提醒卡滞留时长可配置 | 普通提醒卡显示多久后自动消散可设(2/3/4/5/8/10/15/30 秒、自定义 1–600 秒或「常驻」),默认 4 秒;决策红卡恒常驻 |
| ✅ 提醒卡可点击跳转 | 点击界面提醒卡直接跳转到该提醒所属的会话 |
| ✅ 系统通知点击后自动定位 | 点击 Windows 通知横幅会把 DSH 拉到前台,客户端检测到窗口重新聚焦后自动跳到「等你决策」的会话 |
| ✅ 标题闪烁 / 浏览器通知 | 需要决策时浏览器标签页标题闪烁;不在当前标签页时通过浏览器 Notification API 推送 |
| ✅ 决策检测三通道 + DOM 兜底 | approval/request、ask_user_question、确认类工具,外加 MutationObserver 监听审批弹窗,确保决策提醒不漏报 |
| 能力 | 描述 |
|---|---|
| ✅ 任务计划设置 | 新会话自动弹出设置窗口,填写任务名、计划用时、提醒间隔(只有点「跳过」才会跳过,误触不会) |
| ✅ 连续计时 | 仅模型运行时累加计时,暂停(等待用户输入、审批)不计时,恢复后续计 |
| ✅ 结束统计 | 任务结束自动统计累计实际用时与计划用时的差距(正数 = 超时) |
| ✅ 计划可运行中调整 | 模型可通过 task_plan_set 工具改计划用时,同时取消超时标记 |
| ✅ 重启续计 | 重启后运行中任务接着累计,停机时间不计入 |
| ✅ 记录持久化 | 任务记录写入 ~/.dsh/dsh-task-time-records.json(上限 500 条),重启后保留 |
| 能力 | 描述 |
|---|---|
| ✅ Composer Dock 状态条 | 输入框下方实时显示任务名、运行状态、用时、计划时间、提醒次数 |
| ✅ 任务面板 | 左下角 🗂 按钮打开,进行中 / 已完成分组,点击跳转会话 |
| ✅ 模块开关 | 「任务用时」与「提醒」两大模块可独立开关,只开提醒也能正常工作 |
| ✅ 设置页面 | 每个模块只放它最需要的配置:任务用时里配默认计划用时 / 默认提醒间隔,提醒里配提醒卡滞留时长 / 外部通知时机 / AUMID / 测试通知 |
| ✅ 孤儿任务清理 | 会话已删除时自动收尾残留任务,任务面板不显示僵尸任务 |
dsh plugin add dsh-task-time
dsh plugin add https://github.com/Flandern1211/dsh-task-time
git clone https://github.com/Flandern1211/dsh-task-time.git
cd dsh-task-time
dsh plugin add .
安装后重启 DeepSeek Harness,插件自动加载。
点击左下角 🗂 按钮打开任务面板,查看全部任务:
当以下情况触发时,插件自动发出「需要你决策」提醒:
approval/request 事件 — 沙箱升级、权限审批等ask_user_question 调用 — 模型向用户提问ask_user_question / confirm / approve)[data-approval-key])—— MutationObserver 兜底检测决策提醒特征:
= 0,只能点 × 或点击卡片关闭);默认 4 秒,对齐 DSH 内置 Toast 的保持时长autoMs=0,不受滞留时长配置影响),只能点 × 关闭;决策处理完(host 推「决策已处理」)后自动收掉,标题停止闪烁通知按「人是不是正在 DSH 里」自动分流,不会在你看得见界面的时候还弹系统通知:
| 人在哪 | 通道 | 表现 |
|---|---|---|
| 在 DSH 里(页面可见,默认还要求窗口在前台) | 内部通知 | 右下角提醒卡(决策时红色常驻)+ 界面内提示音(WebAudio 合成,不依赖外部文件)+ 需要决策时标题闪烁 |
| 不在 DSH 里(切到别的窗口 / 最小化 / 关掉页面) | 外部通知 | Windows 系统 Toast(含系统提示音)+ 浏览器通知,点击通知后回到 DSH 会自动定位到待决策会话 |
ui-presence(focused / visible);host 超过 10 秒收不到上报就按「人不在 DSH」处理,保证页面关掉后仍然收得到系统通知ui-presence 的返回值同步回 client,两边结论一致:响了内部提示音就不会再发系统通知,反之亦然DSH 不在前台时(默认,切走就发)或 只有 DSH 页面不可见时(只要 DSH 还看得见就不发);设置页「当前状态」实时显示此刻算不算「在 DSH 里」ai.deepseek.dsh.desktop)发出通知dshjump:<sessionId>,点击会把 DSH 窗口置前;客户端在检测到「窗口重新获得焦点」时,先消费 get-pending-jump 的跳转请求,没有请求就直接跳到「正在等你决策」的会话。之所以不做事件回调:Windows PowerShell 5.1 无法订阅 WinRT 通知事件(见「已知问题」)| 场景 | 声音 |
|---|---|
| 任务结束 | Notification.IM |
| 定时提醒 | Notification.Reminder |
| 决策提醒 | Notification.Looping.Alarm |
| 超时提醒 | Notification.Looping.Alarm2 |
| 状态 | 颜色 |
|---|---|
| 运行中 | 🟡 #eab308 |
| 已暂停 | 🔵 #60a5fa |
| 需要决策 | 🔴 #f87171 |
| 已完成 | 🟢 #4ade80 |
| 超时 | 🟠 #fb923c |
~/.dsh/dsh-task-time-records.json插件每 3 秒检查一次存活会话,自动清理:
task_plan_set模型可在运行中通过此工具调整当前任务的计划用时。
{
"plannedMinutes": 30,
"note": "任务说明(可选)"
}
overdueFired = false)此工具已注册到模型 system prompt,会在合适的时机自动调用。
插件分为 任务用时 与 提醒 两大模块,可在设置中自由开关,两者完全独立。
在 DSH 设置 → 任务用时 → 模块开关与默认配置 中配置:
任务用时
☑ 启用任务用时
☑ 计时引擎(运行/暂停连续计时)
☑ 计划配置(设置弹窗 / 跳过恢复)
☑ 定时提醒(间隔提醒 + 超时提醒)
☑ 任务面板(进行中/已完成 + 操作)
☑ Dock 状态条(输入框下方实时显示)
☑ 孤儿清理(会话已删除的残留清理)
─ 默认配置(新会话设置弹窗的预填值)
默认计划用时 [60 分钟 ▾](15/30/45/60/90/120/180 或「自定义…」)
默认提醒间隔(分钟) [10]
提醒
☑ 启用提醒
☑ 决策检测(三通道 + 客户端独立检测)
☑ 内部通知(在 DSH 里:提醒卡 + 提示音)
☑ 外部通知(不在 DSH 里:系统 Toast + AUMID) ← 外部通知只有这一个开关
☑ 标题闪烁(需要决策时)
Toast 点击跳转(点通知回到 DSH 后自动定位到待决策会话)
─ 内部通知配置(在 DSH 里:提醒卡 + 提示音)
提醒卡滞留时长 [4 秒 ▾](2/3/4/5/8/10/15/30 秒 / 常驻(不自动消散)/ 自定义…)
─ 外部通知配置
外部通知时机 [DSH 不在前台时 ▾](或「只有 DSH 页面不可见时」)
当前状态 [在 DSH 里 → 只发内部通知 / 不在 DSH → 会发系统通知]
通知应用身份(AUMID) [ai.deepseek.dsh.desktop]
外部通知链路 [发送测试通知]
外部通知只有一个开关:提醒模块下的「外部通知」。它的时机、应用身份与测试入口在同一个模块的「外部通知配置」里,不再有第二个会互相覆盖的开关。 通知按「人在不在 DSH 里」走两条通道:在 DSH 里只发内部通知(提醒卡 + 界面内提示音),不在 DSH 里才发系统通知 —— 见上方「通知:内部 / 外部两条通道」。
行为说明
| 配置 | 结果 |
|---|---|
| 只开 提醒、关掉 任务用时 | 只保留决策提醒(红卡 + 系统通知),不显示设置弹窗、任务面板、Dock 状态条 |
| 关掉主开关 | 该模块下所有子开关被屏蔽;子开关保留各自状态,主开关重新打开后恢复原状 |
| 关掉 内部通知 | 右下角不再显示任何提醒卡(决策红卡、定时/超时/结束提醒卡都归它管)、不再响界面内提示音(系统通知仍可用) |
| 关掉 外部通知 | 人不在 DSH 时不再发系统 Toast / 浏览器通知(「发送测试通知」按钮仍可用) |
| 关掉 任务用时 | 不再计时、不产生"任务完成"提醒(没有任务就没有完成事件) |
配置文件:~/.dsh/dsh-task-time-modules.json
⚠️ 改完开关需要重启 DSH Desktop(或刷新页面)才会生效 —— 客户端 bundle 在应用启动时载入,热更新仅在
pnpm run dev:web运行时可用。
在 DSH 设置 → 任务用时 中可调整(配置就放在对应模块里):
| 配置项 | 位置 | 描述 | 默认值 |
|---|---|---|---|
| 默认计划用时 | 任务用时 → 默认配置 | 新会话「设置任务计划」弹窗的预填值(预设下拉或自定义) | 60 分钟 |
| 默认提醒间隔 | 任务用时 → 默认配置 | 累计运行多长时间提醒一次(分钟),也是设置弹窗的预填值 | 10 |
| 提醒卡滞留时长 | 提醒 → 内部通知配置 | 普通提醒卡显示多久后自动消散(秒,1–600;0 = 常驻不自动消散);决策红卡恒为常驻,不受影响 | 4 |
| 内部通知 | 提醒 → 模块开关 | 在 DSH 里时的提醒卡 + 界面内提示音 | 开启 |
| 外部通知 | 提醒 → 模块开关 | 不在 DSH 里时的系统通知(唯一开关,开/关只看它) | 开启 |
| 外部通知时机 | 提醒 → 外部通知配置 | DSH 不在前台时(默认)/ 只有 DSH 页面不可见时 | unfocused |
| 当前状态 | 提醒 → 外部通知配置 | 实时显示此刻算不算「在 DSH 里」(只读,5 秒刷新) | — |
| 通知应用身份(AUMID) | 提醒 → 外部通知配置 | 系统通知的应用身份 | ai.deepseek.dsh.desktop |
| 外部通知链路 | 提醒 → 外部通知配置 | 「发送测试通知」按钮,手动验证 Toast + 提示音(不受时机限制) | — |
AUMID(AppUserModelId) 是 Windows 通知系统的关键标识:
~/.dsh/dsh-task-time-records.json| 字段 | 类型 | 说明 |
|---|---|---|
sessionId | string | 会话 ID |
taskName | string | 任务名称 |
startedAt | string (ISO) | 启动时间 |
finishedAt | string (ISO) | 完成时间 |
plannedMs | number | 计划用时(毫秒) |
actualMs | number | 实际用时(毫秒) |
diffMs | number | 差距(毫秒,正数 = 超时) |
{
"records": [ ... ], // 已完成记录
"configs": { ... }, // 会话配置(含 configured 标记)
"dismissed": [ ... ], // 跳过设置的会话列表
"tasks": { ... }, // 进行中任务
"history": { ... }, // 会话历史
"rootSessions": [ ... ], // 根会话 id(用于状态裁剪)
"defaults": { // 全局默认配置
"reminderIntervalMinutes": 10, // 默认提醒间隔(分钟)
"plannedMinutes": 60, // 默认计划用时(分钟)
"reminderAutoDismissSeconds": 4, // 提醒卡滞留时长(秒,0 = 常驻不自动消散)
"toastAppId": "ai.deepseek.dsh.desktop",
"externalWhen": "unfocused" // unfocused | hidden
}
}
configs/dismissed/rootSessions按会话累积,加载时会裁剪到 200 条(活跃会话优先保留);只回写上面这些已知字段,历史遗留键不再被写回磁盘。
dsh-task-time/
├── lib/
│ ├── index.js # Host 端:计时、提醒、持久化、RPC、工具注册、prompt section
│ ├── index.d.ts # Host 端类型声明
│ ├── client.js # Client 端:浏览器 UI(React)、提醒卡、任务面板、设置页
│ ├── client.d.ts # Client 端类型声明
│ └── toast.ps1 # Windows WinRT Toast 通知脚本(PowerShell 5.1+)
├── scripts/
│ ├── verify.mjs # Host 全链路:legacy 迁移 / 状态恢复 / 决策提醒 / 写入 / 孤儿清理
│ ├── verify-modules.mjs # 模块开关 + 默认配置 + 决策检测三通道 + 通知跳转链路
│ ├── verify-channels.mjs # 内部/外部通知分流(无头实例 + Toast 打桩)
│ ├── verify-timing.mjs # 计时 / 间隔与超时提醒 / 结束统计 / 配置边界 / 提醒卡滞留时长
│ ├── verify-dom.mjs # 客户端 UI(mock React + DOM):提醒卡生命周期、设置弹窗、Dock 状态条
│ └── test-changes.mjs # 变更合规性检查(源码级断言)
├── cordis.patch.yml # 插件 bundle patch(插入 host composition)
├── package.json # npm 包配置
├── README.md # 本文件
├── LICENSE # MIT License
└── .gitignore
┌──────────────────────────────────────────────────┐
│ DSH Desktop │
│ ┌─────────────────────────────────────────────┐ │
│ │ Host 进程 (Electron) │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ dsh-task-time (Host) │ │ │
│ │ │ • 计时 & 提醒 (3s tick) │ │ │
│ │ │ • 持久化 (防抖 800ms) │ │ │
│ │ │ • 外部通知 (spawn toast.ps1) │ │ │
│ │ │ • HTTP RPC 服务 (/api/dsh-task-time/rpc)│ │ │
│ │ │ • tool: task_plan_set │ │ │
│ │ │ • prompt section 注册 │ │ │
│ │ │ • 事件监听 (agent/created/status/disposed│ │ │
│ │ │ • 决策提醒 (三通道) │ │ │
│ │ │ • 孤儿任务清理 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ HTTP RPC │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Renderer 进程 (Browser) │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ dsh-task-time (Client) │ │ │ │
│ │ │ │ • 设置弹窗 (shell.overlay) │ │ │ │
│ │ │ │ • 提醒卡堆叠 (shell.overlay) │ │ │ │
│ │ │ │ • 任务面板 (shell.overlay) │ │ │ │
│ │ │ │ • 状态条 (composer.dock) │ │ │ │
│ │ │ │ • 设置页 (settings.section) │ │ │ │
│ │ │ │ • 标题闪烁 / 浏览器通知 │ │ │ │
│ │ │ │ • DOM 审批弹窗兜底 │ │ │ │
│ │ │ │ • 每 2s 轮询 + 上报 ui-presence │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │ OS │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Windows Toast (toast.ps1 via powershell) │ │
│ │ • WinRT ToastNotificationManager │ │
│ │ • 点击 → DSH 窗口置前 → client 焦点检测 → 跳到待决策会话 │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
| 层 | 技术 |
|---|---|
| Host | Node.js (ESM), Electron (可选) |
| Client | React (via ModuleLoader), 原生 JS |
| 通信 | HTTP POST RPC (/api/dsh-task-time/rpc) |
| 通知 | Windows WinRT Toast (PowerShell toast.ps1) |
| 持久化 | JSON 文件 (~/.dsh/dsh-task-time-records.json) |
| 打包 | cordis patch (cordis.patch.yml) |
# 构建(如果使用 TypeScript 源码)
npm run build
# 或直接修改 lib/ 下文件(JS 源码无构建步骤)
# 运行全部套件(mock 运行时,无需 DSH Desktop;不会真的弹系统通知)
npm test
# 单独跑某个套件
node scripts/verify.mjs # Host 全链路
node scripts/verify-modules.mjs # 模块开关 + 默认配置 + 决策三通道
node scripts/verify-channels.mjs # 内部/外部通知分流
node scripts/verify-timing.mjs # 计时 / 提醒 / 配置边界 / 滞留时长
node scripts/verify-dom.mjs # 客户端 UI(提醒卡生命周期、设置弹窗、Dock 状态条)
node scripts/test-changes.mjs # 变更合规性检查
npm test 串跑 6 个套件(verify → verify-modules → verify-channels → test-changes → verify-timing → verify-dom),全部通过才返回 0:
插件作为 profile bundle 安装,通过 cordis.patch.yml 插入 host composition:
# cordis.patch.yml
- insert:
- id: dsh-task-time
name: 'dsh-task-time'
| 事件 | 用途 |
|---|---|
agent/created | 新会话创建时检查是否需要弹出设置窗 |
agent/status | 监听运行/暂停状态切换,累计计时 |
agent/disposed | 真实根会话关闭时自动结束任务并归档(子会话/subagent 收工不算任务结束:按会话头血统识别后静默丢弃,不写记录也不发提醒) |
approval/request | 需要审批时触发决策提醒 |
internal/service | 监听 userQuestions 服务注册,hook ask 方法 |
tools/pre-execute | 确认类工具执行前兜底触发决策提醒 |
所有通信通过 HTTP POST POST /api/dsh-task-time/rpc,请求体格式:
{ "method": "methodName", "args": { ... } }
| 方法 | 用途 |
|---|---|
get-pending-setup | 查询会话是否待设置 |
get-pending-sessions | 查询所有待设置会话 |
get-status | 获取某会话计时状态 |
get-reminders | 获取增量提醒 |
get-pending-jump | 获取待处理的 toast 点击跳转(兼容/扩展通道,见「已知问题」) |
get-latest-decision-session | 当前「等你决策」的会话(点击通知回到 DSH 后用它定位) |
get-config | 获取全局配置 |
set-config | 设置全局配置(非法值直接忽略:提醒间隔/计划用时必须是 >0 的分钟数;reminderAutoDismissSeconds 必须是 >=0 的秒数,0 = 常驻,过大夹到 600) |
get-session-config | 获取会话配置 |
set-session-config | 设置会话配置(计划用时必填,缺失/<=0 返回 ok:false + 可读错误) |
dismiss-session-setup | 跳过会话设置(仅由「跳过」按钮调用) |
reopen-session-setup | 撤销「已跳过」,重新打开设置弹窗(计时条「⚙ 设置计划」) |
get-history | 获取会话历史(保留接口,客户端暂未调用) |
get-pending-sessions | 查询所有待设置会话(保留接口,客户端暂未调用) |
get-task-board | 获取任务面板数据 |
end-task | 结束任务 |
drop-task | 移除残留任务 |
delete-task-record | 删除已完成记录(同时清掉 history 并落盘) |
clear-task-records | 清空全部已完成记录 |
test-toast | 测试通知 |
trigger-decision-alert | 触发决策提醒 |
toast.ps1 保持纯 ASCII,非 ASCII 注释会导致 ANSI/GBK 解码错误Register-ObjectEvent 直接报 "Windows PowerShell cannot subscribe to Windows RT events";用 TypedEventHandler 委托转换虽然能 add_Activated 成功,但回调永不执行(WinRT 在无 PowerShell runspace 的线程上触发)。因此 toast.ps1 不做点击回调,点击定位改由「窗口重新聚焦 + get-latest-decision-session」实现;get-pending-jump 保留为兼容通道(谁写文件谁生效,TTL 5 分钟)set-config 会忽略 <=0 的值,过大值夹到 24 小时;set-session-config 缺计划用时直接拒绝(弹窗会在本地先拦一次并给出提示)set-config 接受 0(= 常驻)与 1–600 秒,负数/非数忽略;「需要你决策」红卡恒为 autoMs=0,不受该配置影响。上限 600 秒是因为再长实际等同于常驻,直接选「常驻」更清楚agents.roots() 不是「用户会话」判据(2026-09 踩坑,v0.2.10 修复):dsh-agent 的 AgentsRegistry.roots() 只表示「运行时没有 owner 的顶层 agent」,其源码注释明确写了它不反映持久化会话血统,subagent 会话同样在其中。判断"这个会话算不算用户任务"必须用会话头字段 origin === 'subagent' / delegationDepth > 0;也不能改用 parentSession —— 用户 fork 出来的会话同样带 parentSession(另有 seedLength),属真实会话。sessions 服务(dsh-session 的 SessionStore)的 get(id) 返回带 .header 的 Session,可用来读这些字段;已关闭的会话不在 store 里,读不到时按"不排除"处理isRoot()(agentsSvc.roots() 成员资格),但 dsh-agent 的 AgentsRegistry.roots() 只按「运行时没有 owner」过滤(源码注释明确写了它不反映持久化会话血统),而 subagent 会话在运行时同样是无 owner 的顶层 agent —— roots() 里照样有它。后果:每个 subagent 都被建了任务、agent/status 一 running 就开始计时,subagent 一收工 agent/disposed 就报一次「✅ 任务结束」(界面卡 + 系统通知)并写一条"已完成"记录(无名、计划 0)。实测一次 fan-out 4 个 subagent 会在约 10 分钟内陆续报 4 次"结束",而父会话自己的任务一直在跑origin === 'subagent'(dsh-session 里该字段的唯一合法值)或 delegationDepth > 0(顶层 0、子会话 1、嵌套子会话 2…)。不能改用 parentSession 判定:用户 fork 出来的会话同样带 parentSession(并额外有 seedLength),但那是真实会话,必须照常计时agent/created / agent/status / agent/disposed(优先读 live agent 自带的 agent.session.header,不依赖服务)以及 tick 孤儿清理 / get-task-board / get-status / get-pending-setup / get-pending-sessions(走 sessions 服务 SessionStore.get(id).header,读到的头部会缓存)。读不到任何会话头时保持原行为(不排除),绝不误杀真实任务rootSessions 里混入的 subagent id,由 tick 静默清理:不写"已完成"记录、不发提醒(resolveRootSession 也不再因为 rootSessions 里有 subagent id 就把子会话当根会话返回)get-status 返回 lineage 诊断字段(headerRead / headerOrigin / delegationDepth / parentSession / isSubagentSession):headerRead:false 说明会话头没读到、判定退回了保守行为,可据此确认修复是否真的生效scripts/verify-timing.mjs 新增场景 P(12 项):子会话不进设置弹窗候选、不开始计时、收工不写记录/不发「任务结束」卡/不发系统通知、遗留幽灵条目被静默清理;并以 fork 会话与真实根会话作对照,防止"一刀切"误杀。脚手架新增可注入的假 sessions 服务。回归强度:把 lib/index.js 回退到修复前,场景 P 精确失败 7 项(含 ✅ 未命名任务 · 任务结束 那条系统通知断言)🔔 提醒(主功能)、⏱ 任务计时(主功能)、🧩 配套能力(辅助),内容未删减,只是把主功能提到前面package.json 的 description 同步改写:先说提醒(定时/超时/需要你决策时的红色常驻提醒 + 内外通知分流 + 卡片可点击跳转),再说任务计时(会话级计划用时、仅模型运行时连续计时、结束统计与计划的差距、本地持久化),与市场条目 data/plugins/Flandern1211__dsh-task-time.yml 的 description.en / description.zh 保持一致lib/ 与 scripts/ 未动,功能与 v0.2.8 完全相同defaults.reminderAutoDismissSeconds,host 每条普通提醒卡按它下发 autoMs,重启后从状态文件恢复pushReminder 写死 4000,client 本地提示卡(删除/结束/移除失败、测试通知)写死 6000/8000 —— 同一个插件里三种时长且都改不了。现在 host 下发的 autoMs 优先,client 本地卡回落到同一个配置值(启动时 get-config 同步一次,设置页改动即时同步)autoMs=0),不受该配置影响——几秒后消散的红卡对离开电脑的用户等于没提醒pushReminder() 里直写 DOM 的旧路径,界面上会叠出两张红卡(一张 bottom:76px、一张 bottom:16px,后者还盖住左下角 🗂 任务面板按钮),而且点一张的 × 另一张不消失。现在只保留 React 一条渲染路径❓ 需要你决策。现在 client 收到 decision-cleared 会收掉该会话的红卡,host 也会把该会话的 needDecision 提醒从队列里摘掉(否则刷新页面重新拉取时红卡会"复活",get-latest-decision-session 也会指向过期会话)approval/request 检测(负 id)与 host 轮询提醒(正 id)没有合并。现在 pushReminder 对同一会话只留最新一条决策卡ReminderGate 原来用的是「reminderUI 或 intervalReminder」,定时提醒开着就照样显示卡片;直写 DOM 的决策卡更是完全不看开关。现在提醒卡统一归「内部通知」管,与文档一致end-task 的 diffMs 符号反了(写成 plannedMs - actualMs,与 finishTask 和文档的「正数=超时」相反)delete-task-record 在清理 history 之前落盘,且之后不再落盘 → 磁盘上残留已删除的记录,重启后"复活"plannedMinutes: null 存下来并标记 configured=true,结果是任务永远不启动(不计时、不提醒、也不再弹设置窗),确认文案还写成"计划用时 null 分钟"。现在 host 拒绝非法计划用时(ok:false + 可读错误),client 在弹窗里先本地拦截并给出提示,弹窗不关闭set-config 不校验提醒间隔:0 / 负数会被存下来,随后 intervalMs <= 0 让定时提醒静默失效(开关显示开着却再也不提醒)。现在非法值直接忽略、过大值夹到 24 小时;状态文件里遗留的非法值在加载时修正为默认值dismissed / configs / rootSessions 按会话累积、从不清理(实测几十个会话后 dismissed 已有 70 条)。现在加载时裁剪到 200 条(保留活跃会话),并且只回写已知字段,历史遗留键(已移除的 externalAlert、旧版默认值快照)不再被一次次写回磁盘hidden(只有页面不可见才发),注释、README 三处与设置页都写默认 unfocused。按文档与既有落盘配置统一为 unfocusedget-pending-jump 读的跳转文件已经没有任何写入方(toast.ps1 回退为 fire-and-forget 后没人写),客户端也从不调用 host 的 get-latest-decision-session。现在客户端在「窗口重新获得焦点」时先消费跳转请求,没有再跳到「正在等你决策」的会话;toast.ps1 与 README 里的相关说明同步改写(并记录 PowerShell 订阅不到 WinRT 事件这一实测结论)npm test 从 4 个套件扩到 6 个:新增 host 全链路套件(101 项:计时/提醒/记录/工具/决策/边界/瘦身)与客户端 UI 套件(39 项:真实 DOM + mock React 渲染决策卡与设置弹窗);旧套件补上系统通知打桩——在此之前跑测试会真的在桌面上弹 Windows 通知console.log:host 端 6 处(get-pending-setup 每次弹窗轮询都打一行、两条 stale waitingDecision 分支、三处 cleared waitingDecision)、client 端 3 处(SetupManager poll 每 2 秒打一行并 JSON.stringify 全部会话 id、get-pending-setup 结果、approval/request 到达)——这些在 DSH 控制台里会持续刷屏,且 SetupManager poll 的字符串拼接本身有开销console.warn、session/event 与 approval/request 处理异常的 console.error).npm-cache/(暂存的 npm 缓存,已加入 .gitignore)与 _dbg_channels.mjs(调试探针,功能已被 scripts/verify-channels.mjs 覆盖)clearWaitingDecision 三处调用点残留的缩进reopen-session-setup RPC 会撤销「已跳过」标记并重新弹出设置窗get-status 增加 configured 字段;设置弹窗点击遮罩时给出提示文案defaults.externalAlert / session.externalAlert 已移除defaults.plannedMinutes,get-pending-setup 下发 defaultPlannedMinutes)ui-presence(焦点 + 可见性,超时 10 秒按离开处理),host 用 shouldSendExternal() 统一判定——人在 DSH 里只发内部通知(提醒卡 + WebAudio 合成提示音),不在 DSH 里才发系统通知;判定结果回传 client,两个通道不会同时响defaults.externalWhen):DSH 不在前台时(默认)或 只有 DSH 页面不可见时;设置页「当前状态」实时显示此刻算不算「在 DSH 里」内部通知(在 DSH 里:提醒卡 + 提示音) / 外部通知(不在 DSH 里:系统 Toast + AUMID);浏览器通知也归外部通道(人在 DSH 里不再弹);「发送测试通知」不受时机限制stateLoaded)才落盘,且停用落盘改用 RECORDS_FILE 而不是另写一份硬编码路径get-config / get-external-status 现在会 await ensureLoaded(),设置页不会先显示内置默认值再跳变end-task 缺少 history 截断,已完成记录可无限增长decisionAlerts)只增不删导致内存泄漏 + 新决策被旧节流压制set-session-config/dismiss-session-setup 中 persistRecords() 无 await,即时落盘保证不可靠setTimeouttools/pre-execute 正则未锚定,子串匹配可误触发决策提醒MIT License — 详见 LICENSE
Copyright (c) 2026 dsh-task-time contributors